1

I have the following (the 'd' method is like 'puts' -- using log_buddy):

response_array.count.times do |i|
      d {i}
      if i == 0
        return_response = response_array[i][:value]['value']
        d {return_response}
      else
        d {return_response; response_array[i][:value]['value']}

        return_response = return_response + "X-LINE-BREAK." + 
                          response_array[i][:value]['value'] 

        d {return_response}
      end

    end

Output looks like this:

response_array.count = 2
i = 0
return_response = "this is the return response"
i = 1
return_response = nil

Problem: the value of return_response is nil when looping the second time i=1. I can't think through why.

Satchel
  • 16,414
  • 23
  • 106
  • 192
  • For readers unfamilar with `log_buddy`, the docs are [here](http://www.rubydoc.info/gems/log_buddy/0.7.0). – Cary Swoveland Feb 24 '15 at 19:34
  • How is supposed to behave `d{}` when there are two statements inside? Is it different if you do `d{return_response}` and `d{response_array[i][:value]['value']}` ? – dgmora Feb 24 '15 at 20:33
  • no; if it is delimited by `;` it runs them as if separate lines. – Satchel Feb 24 '15 at 21:02
  • Thanks for the links to the docs, that helps to understand the multiple statements. It is clear to me that return_response is nil right after the else statement, unless it has be declared outside of the loop. I'm wondering if there is anything else being printed out. In the else statement I would expect return_response + "X-LINE-BREAK." ... to throw a NoMethodError, because return_response is nil. – prettycoder Feb 24 '15 at 21:05
  • When I type the lines IRB it works as expected. I don't know why it would have to be declared out the loop. Could someone explai. – Satchel Feb 25 '15 at 02:21

1 Answers1

2

TL;DR Blocks reset their scope on each iteration. You can do return_response = nil prior to entering the loop, and it should fix your problem because it won't get reset every iteration.

This is a really interesting question because the output is somewhat misleading. I did some more research because I didn't like my answer, and this is what I came up with. (FYI, I ran all this on Ruby 2.1.1)

Variable Presence

In Ruby, variable presence in a single scope is determined by the parser. There is a really good explanation here, but for a simple example:

if false
  fnord = 42
end
fnord # => nil

Contrary to this (you'll have to reload irb or otherwise reset the scope)

fnord # => NameError: undefined local variable or method `fnord' for main:Object
if false
  fnord = 42
end

It's interesting behavior, and I highly recommend reading the above linked stack overflow answer for more details.

Block Scoping

Blocks in Ruby reset their local scopes every iteration.

5.times do
  # This "if false" is here because otherwise the scope reset would cause x 
  # to be a NameError, as above. If you delete the following 3 lines it
  # will raise that error when running "p x".
  if false
    x = 10 
  end
  p x
  x = 20
end

The x = 20 is never retained - you'll get nil printed, five times in a row, because x has been parsed (so you won't get the NameError above) but it hasn't been assigned. You could define x outside of the block scope to keep the value (or even just let the parser parse x outside the block scope, without actually doing an assignment!)

x = nil
5.times do
  p x
  x = 20
end

That will result in nil, then 20, 20, 20, 20. The scope of x in this case is outside the block scope (and blocks can access local variables in the scope around them). Even if you'd used the if false mechanism to "declare" x without assigning it, it would still work.

So, for your situation, the issue isn't that return_response is being scoped to the if section of the if/else statement, it's that it isn't assigned at all the second time around (but it does get parsed, so it has a value of nil, rather than raising a NameError). Let's simplify your situation a bit:

(0..1).each do |i|
  if i == 0
    x = 10
    p x
  else
    p x
  end
end

This prints 10, and then nil, which mirrors the problem you were having. You couldn't fix it by just "declaring" the variable prior to the if statement, but still inside the block scope (or by using block-local variables, which would do the same thing):

(0..1).each do |i|
  x ||= nil # note: this is not something you'd probably do in Ruby

  if i == 0
    x = 10
    p x
  else
    p x
  end
end

This still prints 10, then nil. You have to move the variable outside the block scope for it to not get reset each time.

x = nil # outside the block scope
(0..1).each do |i| 
  if i == 0
    x = 10
    p x
  else
    p x
  end
end

This will print 10, 10.

It looks like from your code you're wanting to build up the return_response from each row as you iterate. You can do return_response = nil prior to entering the loop, and it should fix your problem because it won't get reset every iteration. A more Ruby-like approach would be to assign return_response = [] prior to entering the loop and appending each record (you wouldn't need to check for i == 0 in this case). And if you didn't need the intermediate logging, you could probably get away with this

return_response = response_array.map { |response_line| response_line[:value]['value'] }.join("X-LINE-BREAK.")

I.e. map each item in the response array to its [:value]['value'], then join all those into string with "X-LINE-BREAK." in between each.

PS: These two answers also provide some information on other peculiarities with block scoping, variables, and loops, though they don't apply to your particular case: https://stackoverflow.com/a/1654665/4280232 and https://stackoverflow.com/a/21296598/4280232.

The previous answer I wrote here was incorrect, so I replaced it. If that was the wrong approach, please let me know.

Community
  • 1
  • 1
alexcavalli
  • 526
  • 6
  • 10
  • Hmm that's interesting. So if something is declared in the if statement it remains local? – Satchel Feb 25 '15 at 02:23
  • Your illustration definitely shows the same behavior. – Satchel Feb 25 '15 at 02:23
  • yes using an array and then mapping the array into a single string is the right way from wht you wrote, I will try it out! – Satchel Feb 25 '15 at 23:54
  • In the short term, I did what you did which was move it out of the scope block -- but then the enumation was wrong so needed to -1 (because the [0] array was already assigned! – Satchel Feb 25 '15 at 23:54
  • @Angela Glad to help. Yes, try initializing the response to an empty array and << each line, I think you'll find the code to be much more readable. Also, you should probably use `response_array.each do |response_line|` instead of `response_array.count.times do |i|` and `response_array[i]` - it's more in line with the way iterators are used in Ruby. In that case, `response_line` will be equivalent to `response_array[i]`, and adding those values to `return_response` can be done with `return_response << response_line[:value]["value"]`. Good luck! – alexcavalli Feb 26 '15 at 00:15
  • your one line answer to use map is actually the best answer it removes two loop blocks and makes alot more sense. – Satchel Feb 26 '15 at 05:05