I contend that if you are using defined? you are doing something wrong. local variables should be local. If you don't know what exists in the current scope, you are dealing with confusing code. Rails erb evaluation for partials makes this mistake, and creates a poor reimplementation of function calls where the function header is defined by the caller with the :locals =>{} hash.
Or at the very least you are likely to get terribly confused. The posters confusion has absolutely nothing to do with conditionals and modifiers and is entirely about Ruby variable definition and scoping semantics. In fact if you broke one of his examples by reversing the conditional, the variable, the assignment of which is never evaluated is still defined.
>> a = 1
=> 1
>> unless defined? a
>> b = 5
>> end
=> nil
>> defined? b
=> "local-variable"
>> b
=> nil
That makes sense, but I've never seen statement modifiers defined that way. Every explanation I've ever seen, the pickaxe included, seems to outright state, or at least imply, the former.
I think it's easy to get confused here because there's a natural tendency to read and interpret 'unless' as English and the fact that assignment in Ruby is also implicit declaration. Once this happens -
a =
a is defined. It doesn't matter at all what comes after (as long as it keeps the expression syntactically valid) - a is defined. The only question remains is what will end up getting assigned to it. Try
a = asdfgasdfweradsf
it will complain there is no asdf... but a is defined.
That definitely sounds like a bug to me. Either a statement is fully valid and is executed, or it isn't and it isn't. In Python:
>>> a=b
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined
>>> a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>>
If it's a bug, it's by design. Note that this only happens with local variables (rather than instance, class or global variables) according to David Black's The Well-Grounded Rubyist, page 153 ("Assignment syntax in condition bodies and tests" - everything that follows, including emphasis is from the book):
Ruby doesn't draw as clear a line as compiled languages do between "compile time" and "run time," but the interpreter does parse your code before running it, and certain decisions are made during that process. An important one is the recognition and allocation of local variables.
When the Ruby parser sees the sequence identifier, equal-sign, value, as in this expression
x = 1
it allocates space for a local variable called x. The creation of the variable - not the assignment to it, but the internal creation of a variable - always takes place as a result of this kind of expression, even if the code isn't executed!
Consider this example:
if false
x = 1
end
p x # Output: nil
p y # Fatal error: y is unknown
The assignment to x isn't executed, because it's wrapped in a failing conditional test. But the Ruby parser sees the sequence x = 1, from which it deduces that the program involves a local variable x. The parser doesn't care whether x is ever assigned a value. It's job is just to scour the code for local variables for which space needs to be allocated.
The result is that x inhabits a strange kind of variable limbo. It has been brought into being and initialized to nil.
It's a fairly common phrase (I thought - maybe not) simply meaning: you or I or everyone may not like it, but the developers did it that way on purpose.
In many cases, a bug by design is a question of trade-offs. In this case, stricter rules for the parser might mean more syntax for the programmer.
Depends what you are astonished by, I suppose. As far as I can tell the Ruby behaviour has to do with the fact that Ruby variable definition appears to take place sometime before evaluation, maybe during parsing. So as long as the statement parses and looks like a variable definition/assignment, you get your variable. On evaluation, undefined variables cause barfage but in the case above, the a got defined before the statement was actually executed.
Well, I am astonished that entering invalid code can cause a persistent state change within my interpreter. I can't think of any other REPL language in which that happens. OCaml:
# let a=b;;
Error: Unbound value b
# a;;
Error: Unbound value a
#
Haskell:
Prelude> let a=b
<interactive>:1:6: Not in scope: `b'
Prelude> a
<interactive>:1:0: Not in scope: `a'
Prelude>
Scheme:
guile> (define a b)
Backtrace:
In standard input:
1: 0* (define a b)
standard input:1:1: In expression (define a b):
standard input:1:1: Unbound variable: b
ABORT: (unbound-variable)
guile> a
ERROR: Unbound variable: a
ABORT: (unbound-variable)
Yeah I was a little surprised too. It looks like the parser just defines foo for any parsable foo = .... before anything actually runs. Kind of a funky design choice, especially for such a dynamic language.
ra:~$ irb
irb(main):001:0> defined? a
=> nil
irb(main):002:0> defined? b
=> nil
irb(main):003:0> a = b
NameError: undefined local variable or method `b' for main:Object
from (irb):3
irb(main):004:0> a
=> nil
irb(main):005:0> b
NameError: undefined local variable or method `b' for main:Object
from (irb):5
irb(main):006:0> defined? a
=> "local-variable"
irb(main):007:0>
def ex1
a = 5
if a
puts "a: #{a}"
end
end
def ex2
if a = 5
puts "a: #{a}"
end
end
def ex3
puts "a: #{a}" if a = 5
end
If we start with the code in ex1 and decide to shorten it, ex2 is okay (although some people don't care for assignment in a conditional because it could be confused for a typo). But, ex3 will raise an exception that 'a' has not been defined.
I've just internalized using the longer form when using assignment in a conditional.
I think the article is wrong. His rewrite on line 06. should use if instead of unless.
$ irb
>> b
NameError: undefined local variable or method `b' for main:Object
from (irb):1
>> if defined? b
>> b = 5
>> end
=> nil
>> b
=> nil
>> defined? b
=> "local-variable"
No, it's using unless like I intended. But your example also demonstrates the weird behavior. As a few people have pointed out here and over in the post comments, the issue at hand isn't so much about statement modifiers as it is about how Ruby 'defines' local variables.
Take this for example:
ra:~$ irb
a = irb(main):001:0> a = b
NameError: undefined local variable or method `b' for main:Object
from (irb):1
irb(main):002:0> defined? a
=> "local-variable"
irb(main):003:0>
It just so happens that using statement modifiers in the way I tried using them brings this behavior to light.