Andrew
Andrew

Reputation: 43153

Ruby method argument default value, implicit vs literal nil, library's responsibility or callers?

I'm hoping you guys can help me decide which behavior would be more "standard" or "expected."

Background:

In Ruby you can provide a default argument to a method, eg.

def tester(input={})
  input
end

If you call tester you get {}. However if you call tester(nil) you get nil.

Honestly that surprised me, I figured you'd get {} if you passed nil to the method, because I always imagined the statement input={} in the method definition to be akin to input ||= {}, but clearly it's more like defined?(input) ? input : {}.

Question:

In a library I maintain I've got a class that takes an optional hash on initialization. The initializer looks just like tester above:

def initialize(input={})

So you can call MyClass.new or MyClass.new foo:1, but you can't call MyClass.new nil

That seems obvious except when using variables to init, eg. MyClass.new(opts) where opts could be a hash or nil.

I could change the way this works by changing the implementation like so:

def initialize(input=nil)
  input ||= {}
  ...
end

But, I'm left wondering, is this the right design for a Ruby interface?

Should I instead expect the caller to use MyClass.new(opts || {})?

If you're using a library, and there's a class in that lib that takes an optional hash, would you expect it to be safe to pass nil to the initializer and for that to be treated as equivalent to passing no argument? Or would you expect that to break because, in Ruby, passing a literal nil is not the same as passing no argument?

Which design feels more "correct" to you?

Upvotes: 1

Views: 2335

Answers (1)

tadman
tadman

Reputation: 211740

The defaults only apply to unspecified arguments, not those that are supplied but nil. In Ruby that's a value, an object even, it's not like other languages with "undefined" or "null".

Your observation to use ||= { } is how a lot of libraries handle this sort of thing. I usually design code to be resilient in the face of passing nil in by accident as this can happen by mistake and seems like a reasonable interpretation of how to do what I mean without being overly paranoid and defensive.

Normally this is only an issue with = { } or = [ ] type situations. In a sense this is the Ruby principle of passing in something that's close enough to work, or at least not incorrect. Think of how the method might be used and accommodate all reasonable situations.

So the signatures you tend to see look like this:

def with_options(args = nil)
  # Default arguments to an empty Hash
  args ||= { }

  # ...
end

def trim(string, length = 1)
  string = string.to_s # Coerce to string
  length = length.to_i # Coerce to integer

  # ...
end

For code that's used internally it's generally best to be more strict and throw errors if the methods are used incorrectly, that is be less defensive, but for methods that are to be used by a variety of parties that may not get everything perfectly correct, a degree of latitude on what comes in is always appreciated.

Passing in "1" instead of 1 for a numerical argument is all too easy in an environment like Rails, and punishing someone for not casting properly is perhaps too cruel. At least try and convert and deal with the result.

Upvotes: 4

Related Questions