Post

Is it possible to conditionally define a local variable in Ruby?

Let’s say you wish to conditionally define a local variable in Ruby. Why would you need that? That’s beside the point, it’s mostly a thought excercise that’s an excuse to learn about a specific corner of Ruby. But, if you still want a real use case, you’re being a bit difficult, but let’s say that you’ve got some metaprogramming code that at some points uses defined?(x) calls to do something based on whether a local variable is defined or not. Yes, it’s very contrived.

So let’s try and define a variable based on whether a condition is true or false.

Try 1: just use if

Ruby being so incredibly dynamic, my first thought was that I could just define it behind an if. The true case is trivial:

1
2
3
4
if true
  x = 42
end
puts x.inspect # => 42

And the only thing we need now is for the false case to raise a NameError: undefined local variable or method 'x' and this article is done:

1
2
3
4
if false
  x = 42
end
puts x.inspect # => nil

That didn’t work, it’s defined and has a nil value, although the code defining it clearly didn’t run.

As it does, the official documentation has the explanation:

The local variable is created when the parser encounters the assignment, not when the assignment occurs

Ruby is dynamic, but all that dynamism starts only after the parser is done! Can we cheat the parser?

Try 2: Cheat the parser with eval

Code we eval is only parsed at runtime, maybe that will work?

1
2
3
4
if false
  eval("x = 42")
end
puts x.inspect # => raises NameError

We fooled it! Now just to check the true case:

1
2
3
4
if true
  eval("x = 42")
end
puts x.inspect # => raises NameError

No luck. It turns out that eval introduces a new scope around the code being eval’d, so the new variable is only local to it.

Try 2: Cheat the parser with binding methods.

Binding has a local_variable_set method. Let’s try that:

1
2
3
4
if false
  binding.local_variable_set(:x, 42)
end
puts x.inspect # => raises NameError

Good so far. And the true case:

1
2
3
4
if true
  binding.local_variable_set(:x, 42)
end
puts x.inspect # => raises NameError

No luck. Second look at the documentation explains it:

1
2
bind.local_variable_set(:b, 3) # create new local variable `b'
                               # `b' exists only in binding

Conclusion

We’re out of options, so the answer is that it can’t be done. Most likely, any code relying on defined? to run conditional logic should probably think if there is a cleaner way that is less likely to hit against the limits of Ruby dynamism.

Bonus: unpredictable behaviour of defined?

Remember the contrived case from the beginning of the post? It might look something like this:

1
2
3
4
5
# Only set the value of x if it was previously defined.
if defined?(x)
  x = true
end
puts x.inspect # => nil

So far so good. And you would of course rewrite this into a one liner:

1
2
x = true if defined?(x)
puts x.inspect # => true

Wait, it gives a different result? Those two expressions should be always identical. What’s going on? The answer lies in the generated RubyVM instructions:

1
puts RubyVM::InstructionSequence.compile("if defined?(x); x = true; end").disasm

Gives:

1
2
3
4
5
6
7
8
9
10
11
12
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,29)> (catch: false)
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] x@0
0000 putself                                                          (   1)[Li]
0001 defined                                func, :x, true
0005 branchunless                           13
0007 putobject                              true
0009 dup
0010 setlocal_WC_0                          x@0
0012 leave
0013 putnil
0014 leave

While:

1
puts RubyVM::InstructionSequence.compile("x = true if defined?(x)").disasm

Gives:

1
2
3
4
5
6
7
8
9
10
11
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,23)> (catch: false)
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] x@0
0000 putobject                              true                      (   1)[Li]
0002 branchunless                           10
0004 putobject                              true
0006 dup
0007 setlocal_WC_0                          x@0
0009 leave
0010 putnil
0011 leave

Notice that in the first case, the defined? function is called. In the second case, because parser just parses top to bottom, left to right, it has already parsed the variable declaration and entered it into the local variables table by the time it gets to the if keyword. It then optimises the defined? call by resolving it to true at compile time. The rule is that parser defines the variable and it does it in the parsing order, regardless of the execution order. So, the optimisation means that actual implementation of defined? never runs.

And that’s another way in which being clever with defined? can turn against you.

This post is licensed under CC BY 4.0 by the author.