Variable scope and order of parsing vs. operations: Assignment in an "if"
Asked Answered
G

3

4

My understanding is that the if statements at the end of the line are evaluated before the code at the front of the line:

'never shown' if (false)

And assignment is possible in an if statement.

'shown' if (value = 'dave is king')
value #=> "dave is king"

And, when a variable that doesn't exist is assigned to, it is created. There is no need for it to exist beforehand. Is this true?

If all these assumptions are true, why does this fail?

error_array << error if (error = import_value(value))
#=> undefined local variable or method `error' for

It assigned to error before the array push right? I want to understand when things are evaluated.

This one does work:

if (error = import_value(value))
  error_array << error
end

Now I'm really confused.

Goldfinch answered 27/2, 2013 at 1:49 Comment(14)
error_array << error if( error = import_value(value) ) the if statement equates to true here so you're trying to append error to error_array, but error is nilSaddlebacked
error isn't nil it's what ever is returned from import_value.. Additionally.. appending nil to an array shouldn't cause an exception... Third, why does the it work in the edit.Goldfinch
also.. if import_value returns nil, then the if statement equates to false.Goldfinch
i assume this is because Ruby parses the left-hand side first and expects error to already exist. but it's really moot because you should avoid stupid code-compression tricks and just write the block. vertical space is not a scarce resource.Terzas
there's nothing "stupid" about them, it often makes code more readable, especially when mixed with blocks and method chains, maybe we should go back to numbering our lines too?Eduino
there are heaps of arguments for assignment in if statements.. And there are also heaps of them for writing smaller code blocks.. Finally this is just an example... purely made up for this question.Goldfinch
writing code backwards from the way it's actually run would seem like a giant source of confusion. see, for example, this very question. i heartily support the original perl idioms of last if ... and other control flow, where the action is both dead simple and more important than the condition, but the given example is absurd.Terzas
Hey... c guy here.. the if at the end is about as fun for me as the unless (which still has me confused)... That said.. its the only way to have one line ifs.. I really quite hate the word 'END' scattered all over my code... it makes it harder to read blocks.. IMHO.Goldfinch
You don't have to use end in blocks, you can use { } like C. But you'll find that syntax is only really ever used in one-liners.Eduino
does if allow {}?? I get "unexpected CURLY".. which is almost never a good thing..Goldfinch
It's only for blocks (do end)Eduino
Come on man.. unexpected CURLY.. that's funny.. I skyped my co-worker that error message.Goldfinch
What version of Ruby are you using? This works fine as written in 1.8.7.Longueur
@daveatflow: "its the only way to have one line ifs" – Huh? What's wrong with if error = import_value(value) then error_array << error end?Overshadow
E
5

It only happens when you try to assign a literal value, if you call a function it works.

def foo(a)
  a
end

p 'not shown' if(value = foo(false))
p 'shown' if(value = foo(true))

# This outputs a Warning in IRB
p 'shown' if(value = false)
(irb):2: warning: found = in conditional, should be ==

If you turn on debugging (-d) you will see a warning about an used variable value

warning: assigned but unused variable - value

This "works" because the statement does evaluate to true as far as if is concerned, allowing the code preceeding it to run.

What is happening here is that if() when used as a modifier has it's own binding scope, or context. So the assignment is never seen outside of the if, and therefore makes no sense to perform. This is different than if the control structure because the block that the if statement takes is also within the same scope as the assignment, whereas the line that preceeded the if modifier is not within the scope of the if.

In other words, these are not equivelant.

if a = some(value)
  puts a
end

puts a if(a = some(value))

The former having puts a within the scope of the if, the latter having puts a outside the scope, and therefore having different bindings(what ruby calls context).

Ruby Order of Operations

Eduino answered 27/2, 2013 at 2:4 Comment(8)
No joy on the test code.. same error.. the variable for error doesn't exist.. I suspect you're right about the parser, not the execution path..Goldfinch
1.9.2 locally and Heroku :)Goldfinch
Getting the same error on a freshly launched irb (1.9.3). In @Cluster's case, I suspect error was already defined beforehand somehow.Ancient
No, if i try to call error alone it raises an error. If i use the push op with no brackets it raises an error, if i put it in brackets it executes no problem. Tried it in Ruby 1.8.7, 1.9.2, 1.9.3, rbx and jrubyEduino
@daveatflow I just noticed that too, also ruby 2.0.0-p0 does raise an error in the console.Eduino
How about we just call it a parsing issue... and settle on the fact that if at the end of the line SUCKS :) (that last bit is a joke, it doesn't capital letter suck.)Goldfinch
I think it's a scope issue, if as a modifier has a different scope than if as a block statement. Run ruby with a -d flag and you get test.rb:4: warning: assigned but unused variable - cEduino
I updated my example with a better explanation. It's just a scoping issue, variables defined within a control structure are only available within that control structure, and what comes before an if when it is used as a modifier is not within the scope of the if.Eduino
M
4

In Ruby, local variables are defined by the parser when it first encounters an assignment, and are then in scope from that point on. Since Ruby is parsed like English, left-to-right, top-to-bottom, the local variable doesn't exist yet when you are using it, because the usage is further left from the assignment.

Here's a little demonstration:

foo # NameError: undefined local variable or method `foo' for main:Object

if false
  foo = 42
end

foo # => nil

As you can see, the local variable does exist on line 7 even though the assignment on line 4 was never executed. It was, however, parsed and that's why the local variable foo exists. But because the assignment was never executed, the variable is uninitialized and thus evaluates to nil and not 42.

Now let's get to simplest version of your case:

bar if bar = true
# warning: found = in conditional, should be ==
# NameError: undefined local variable or method `bar' for main:Object

bar # => true

The variable is created when the assignment is parsed, which is here:

bar if bar = true
       ^^^^^^^^^^

But it is used here:

bar if bar = true
^^^

Which is before the assignment. The fact that the assignment is executed before the usage is irrelevant because the parsing is relevant here, and the assignment gets parsed after the usage which means that at the point of usage the parser still thinks it's a method call with no argument list and an implicit receiver (i.e. equivalent to self.bar()) and not a local variable.

Milky answered 27/2, 2013 at 12:16 Comment(1)
I had a comment here, but I asked a new question here.Howlond
A
2

My guess is that the order of parsing is different from the (logical) order of execution. In particular, given

array << error if (error = some_function)

Then logically, execution should go something like

  1. call some_function
  2. if error not defined, define error
  3. assign return value of some_function to error
  4. evaluate if
  5. if if evaluates to true, append value of error to array

However, parsing wise (assuming typical LR parser), it goes

  1. Got token array (identifier). Is this defined? Yes. Is it a variable? Yes.
  2. Got token << (operator). Does array respond to <<? Yes (otherwise, output "undefined method" error).
  3. Got token error (identifier). Is this defined? No. Output "undefined local variable or method".
Ancient answered 27/2, 2013 at 2:22 Comment(4)
unless you're running it in the console for 1.9.x, but not 2.0.x :) Then it must not check.. or something.Goldfinch
No, it is a scope issue, error is not being defined in the context of the << operator, and will never exist. If it's was an order of operations or even parsing error, then you could solve it with brackets.Eduino
But mate.. it does work in the console.. Or well in the 1.9.x console, not the 2.0 console, and not in a script.. Copy script to console and tada, you've got it..Goldfinch
I have no idea why it works in the console. There must be some scope leakage in IRB, which wouldn't be suprising. Guessing they fixed it in 2.0, JRuby has the same issue.Eduino

© 2022 - 2024 — McMap. All rights reserved.