Hacker News new | past | comments | ask | show | jobs | submit login
Ruby statement modifiers behave differently than conditional statements (ryanangilly.com)
26 points by angilly on Dec 28, 2009 | hide | past | favorite | 26 comments



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


Right, variable definition happens as early as when the code is parsed, even if it is never used.


whoa. didn't see that coming.


I don't know whether they're planning to deprecate that but they do provide the alternative local_assigns hash for partials now.


The following is wrong. Only read the rest of this reply if you want to see how dumb I am.

This is not a bug.

"a = 5 unless defined? a" is not meant to be equivalent to this:

  unless defined? a
     a = 5
  end
It is supposed to be (and is) equivalent to this:

  a = unless defined? a
        5
      end*


this is wrong!

  >> a = 1 
  => 1
  >> b = 2
  => 2
  >> a = 3 unless b==2
  => nil
  >> a
  => 1
  >> a = unless b==2
  >> 2
  >> end
  => nil
  >> a
  => nil
The difference is in the variable definition semantics, not in the assignment semantics. Please do not spread this misinformation.


My bad. Thanks for clarifying that.


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
  >>> 
I think that follows the POLA...


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.


Can a bug be by design? :)


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.


For this to count as a feature and not, umm, a dodgy implementation of a parser, it would have to convey some advantage... But what?


As far as I can tell the only advantage is the ability to write code such as this:

  if condition
    x = true
  end
  puts "hello" if x
without initializing x to false before the conditional.


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)

You can see where my astonishment comes from...


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.


interesting. in ruby:

  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>
'a' gets defined.


  >> if false
  >>   a = 1
  >> end

  >> defined? a
  => "local-variable"

  >> a
  => nil


That explains it! It does break the principle of least surprise though...


Yes, they are different. For example:

    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.


I think the main part was already covered, but there is one important detail left uncorrected:

They are expression modifiers and conditional expressions. Everything in Ruby is an expression, not a statement.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: