Ruby时刻:无的论证
Moment of Ruby: An Argument for Nothing

原始链接: https://www.aha.io/engineering/articles/moment-of-ruby-an-argument-for-nothing

在 Ruby 中,方法可以通过接受可选参数同时充当 getter 和 setter。难点在于区分参数被省略与显式传入 `nil` 或 `false`。常见做法是使用哨兵对象,但 Ruby 提供了一种更巧妙的技巧:将默认参数设为包含赋值的表达式,例如 `def cloaking(value = (nothing_provided = true))`。Ruby 只在参数被省略时求值默认值,因此 `nothing_provided` 在该情况下为 `true`,否则为 `nil`。方法随后可以在未提供值时返回存储的值,或存储任何提供的值——包括 `nil` 或 `false`。这种模式出现在 Rails 的 `index_with` 中。它无需哨兵对象,就能清晰地区分“提供了值”与“未提供值”。

Hacker News 新 | 过去 | 评论 | 提问 | 展示 | 工作 | 提交 登录 Ruby 时刻:为 Nothing 辩护 ( aha.io ) 27 分 由 zemptime 4 小时前 | 隐藏 | 过去 | 收藏 | 2 条评论 帮助 matharmin 20 分钟前 | 下一个 [–] 这是个有趣的技巧,但如果是 setter 方法,你真的应该使用 `def cloaking=(value)`,而不是重载同一个方法。如果能给出一个更真实的例子,说明这个技巧可以用在哪里,那就更好了。 回复 semiquaver 11 分钟前 | 上一个 [–] 哇,我写了 15 年以上的 Ruby,却从没见过这个技巧! 回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Last week I learned a technique in Ruby so small and useful, I couldn't believe I hadn't come across it before.

Suppose a method doubles as a getter and setter. Pass a value to update a setting, or pass nothing to read it:

module Cloakable
  def cloaking(value = nil)
      if value
       @setting = value
      else
       @setting
      end
  end
end

That works until we actually want to set false or nil as a value:

class Spaceship
  extend Cloakable
  
  cloaking(true) # Hide.
  cloaking(false) # Wave hello.
  cloaking(nil) # Clear the setting
  cloaking # Check before waving
end

How do I extend the contract of my method to consider that last case without breaking its existing behavior? Checking value.nil? wouldn't solve the whole problem either. An explicit nil and an omitted argument would still look the same.

One way I've seen this done is like so:

module Cloakable
  NOT_PROVIDED = Object.new.freeze

  def cloaking(value = NOT_PROVIDED)
    unless value.equal?(NOT_PROVIDED)
      @setting = value
    end

    @setting
  end
end

But we don't have to do all that. Here's the trick:

module Cloakable
  def cloaking(value = (nothing_provided = true))
    return @cloaking if nothing_provided
      
    @cloaking = value
 end
end

Ruby evaluates a default argument expression only when that argument is omitted. And that expression can include an assignment.

If we call setting without an argument, nothing_provided = true runs. The assignment also evaluates to true, making that the default value of value — though we don't use it in this branch.

If we supply any argument, even false or nil, Ruby skips the default expression. It still recognizes nothing_provided as a local variable, but its value is nil because the assignment never ran.

We can now distinguish all four calls:

  cloaking(true)  # Stores true.
  cloaking(false) # Stores false.
  cloaking(nil)   # Stores nil.
  cloaking        # Returns the stored value.

I first noticed this in Rails' implementation of index_with:

  def index_with(default = (no_default = true))
    if block_given?
      to_h { |elem| [elem, yield(elem)] } # rubocop:disable Rails/IndexWith
    elsif no_default
      to_enum(:index_with) { size if respond_to?(:size) }
    else
      to_h { |elem| [elem, default] } # rubocop:disable Rails/IndexWith
    end
  end

Ruby gives us a neat way to tell the difference when "a value was supplied" and "nothing was supplied" are different pieces of your method's contract. Sometimes, doing nothing is the wisest thing you can do — even if you need to get into a little argument about it.

byroot originally introduced this in Rails PR #44611, and provided some additional guidance on when to use this pattern:

byroot: Yeah, it's used a lot in Ruby-core (for methods we migrated from C to Ruby, as in C it's easier to efficiently check for variadic arguments). Sentinel values have an advantage when the argument is passed to another method or stored in an ivar, but other than that yes that pattern is somewhat superior.


For more insights into how our engineering team works with Ruby, read the Aha! engineering blog.

联系我们 contact @ memedata.com