3

I am studying Ruby, and typed a piece of code to experiment with block-scoped variables:

x = 10
3.times do |i; x|
  x = x + 1
  puts("inside block is #{x}")
end
puts("outside block is #{x}")

I would expect the x block variable to automatically capture the value of the x global variable, and then x inside the block to be 11 every time, and x outside the block to be protected, and stay at 10. This "shielding" action is what is described in many Ruby tutorials found throughout the web.

But instead, the script fails, telling me that x is nil and that it doesn't have a + function. In other words, the x block variable hasn't been initialized with a value.

The exact code above is in a file called delete_me.rb, and ran with: ruby delete_me.rb

When I run the script, I get the following error:

delete_me.rb:3:in `block in <main>': undefined method `+' for nil:NilClass (NoMethodError)
    from delete_me.rb:2:in `times'
    from delete_me.rb:2:in `<main>'

How do I initialize the value of a block variable in Ruby?

2
  • ruby 2.4.4p296 (2018-03-28 revision 63013) [x64-mingw32] on Windows Commented Sep 19, 2018 at 16:47
  • True: then why is x nil inside the block? Commented Sep 19, 2018 at 16:54

2 Answers 2

4

I would expect the x block variable to automatically capture the value of the x global variable,

That is not how Ruby works, and there is no reason to expect it to behave that way. An inner variable never takes its value from a variable it is shadowing, in any language that I know of. That would be a horribly designed language, where unrelated outer context could be changed or introduced that breaks an inner scope in completely unanticipated ways. The entire purpose of scope is to prevent this kind of thing.

x is nil, because it's a newly introduced variable that you didn't assign a value to.

How do I initialize the value of a block variable in Ruby?

You can check if it is nil, and then assign a value to it:

x = 10
3.times do |i; x|
  x ||= 0
  x = x + 1
  puts("inside block is #{x}")
end
puts("outside block is #{x}")

Note that this loop prints "inside block is 1", three times. You're making a new x and setting it to 0 each time the block is invoked. If you want to accumulate state while you iterate, this is the wrong way to go about it. You either want to not shadow the outer x, or use a different Enumerable method like inject or each.with_object.

Sign up to request clarification or add additional context in comments.

3 Comments

Very good answer. You may want to add a sentence to the effect of "the whole point of block-local variables is that they are local to the block and do not capture their lexical surrounding scope."
Huh, so the only purpose of block variables in Ruby, is so that you don't need to rename a global x to a local my_x within the block?
@ouni You should stop referring to x as a global variable here, it isn't. Global variables in Ruby start with a $ sign, ie $x. The purpose of block variables is to define a variable scoped to the block, nothing more. It has nothing to do with variables already declared in the parent scope which happen to have the same name.
0

This is because in ruby blocks do not create a new scope BUT they do create variables in their lookup table.

Your block would have access to the global x BUT you are passing x as an argument to the block. When times iterates it passes the i and x is set to nil because it is not passed a variable. So in your block you have a new x that has a value of nil on each iteration. If you pass in any other named variable it will work.

x = 10
3.times do |i; y|
  x = x + 1
  puts("x block is #{x}")
end
puts("outside block is #{x}")

Or even better, don't define it with another argument unless you are wanting to override any globals

x = 10
3.times do |i|
  x = x + 1
  puts("x block is #{x}")
end
puts("outside block is #{x}")

2 Comments

I'm specifically trying to get the | something, somethine_else; local_var | syntax to work (with the semicolon). The semicolon is not a block argument, it's a block variable, see viewsourcecode.org/why/redhanded/inspect/…
will update with the semicolon, the answer is the same for scope and the reason it is not working

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.