Pangram verdict · v3.3
We believe that this entire text is human-written.
AI likelihood · overall
HumanArticle text · 429 words · 8 segments analyzed
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. Addendum byroot originally introduced this in Rails PR #44611, and provided some additional guidance on when to use this pattern: For more insights into how our engineering team works with Ruby, read the Aha!
engineering blog.