what we blog

Ruby tricks: detect evaluation of default arguments

Default arguments are an often used feature of the Ruby programming language. Their basic syntax is simple:

def foo(bar = nil)
  bar == nil
end

This gives us two ways to make sure that foo evaluates to true:

a = nil
foo #=> true
foo("a") #=> false
foo(a) #=> true

Curious as we are, there is one question left: How do we detect whether the caller passed no argument or that he passed an argument, which just happened to be nil?

So, lets find out how Ruby handles default arguments, by using a different kind of argument and looking at the outcome:

def foo(bar = [])
  bar
end
foo.object_id #=> 2152318880
foo.object_id #=> 2152446600

So, Ruby is clever enough to create a new Array on every call to foo. Is it actually capable to do that with other kinds of code? Yes!

def foo(bar = Object.new)
  bar
end
foo #=> #<Object:0x000001020cad98>
foo #=> #<Object:0x000001020cad98>

This is an example that shows that Ruby is actually a rather strict language: Everything is an expression, even default arguments. So, if everything is an expression, can we actually assign variables in default arguments? Yes, again!

def foo(bar = (batz = Object.new))
  [bar, batz]
end
foo #=> [#<Object:0x00000102140b38>, #<Object:0x00000102140b38>]
foo #=> [#<Object:0x000001020f4d78>, #<Object:0x000001020f4d78>]

Now, lets assemble the pieces: default arguments are expressions that are only evaluated when no argument is given. Variables defined in those expressions are valid within the scope of the method. Which allows us to do the following:

def foo(bar = (bar_evaluated = true; nil))
  if bar.nil? && !bar_evaluated
    warn "You passed bar, but it was nil."
  end
end
foo #=> no warning
foo(nil) #=> warning

Coda in Python

As a little side-note: although having a similar syntax, default arguments in Python are not evaluated on each call, which produces this gem:

def foo(bar = []):
  bar.append("a")
  return bar

foo() #=> ['a']
foo() #=> ['a','a']