Loop resets variable - ruby

I am encountering the following unexpected behavior while running the following loop:
outside_var = 'myString'
loop do
inside_var ||= outside_var
result = SomeCalculation.do_something(inside_var)
inside_var = result[:new_inside_var_value]
end
Now, on the first iteration inside_var gets set to outside_var, which is the expected behavior. Just before the next iteration I set inside_var to something else (depending on the result I got from the calculation inside the loop). This assignment works (printing inside_var at the very bottom of the loop confirms that). On the next iteration, however, inside var goes back to the original state, which is something I didn't anticipate. Why is it doing that and how can I set this variable inside this loop?
I am running Ruby 2.6.5 with Rails 6.

This is a scoping issue. inside_var is scoped to the block. One might check the binding, it changes.
outside_var = 'myString'
2.times do
puts "INSIDE 1: #{defined?(inside_var).nil?} → #{binding}"
inside_var ||= outside_var
puts "INSIDE 2: #{inside_var}"
end
#⇒ INSIDE 1: true → #<Binding:0x000055a3936ee0b0>
# INSIDE 2: myString
# INSIDE 1: true → #<Binding:0x000055a3936edc50>
# INSIDE 2: myString
That said, every time the execution enters the block, the binding is reset, that’s why one should not expect the variables from another scope (with another binding) to exist.

When you do a new iteration inside the loop, you are going to reset everything. I suggest you to modify the var outside the loop to preserve the value inside. Something like this:
result_var = 'myString' # value on the first iteration
loop do
result = SomeCalculation.do_something(result_var)
result_var = result[:new_inside_var_value] # at the end of the first iteration you are already overriding this value
end

Related

Test ruby yield method for block execution

Whenever I pass a block to a yield method in Ruby I would like to know if the block was actually executed. For instance:
def yield_method(list)
list.each do |item|
yield item
end
end
yield_method(ARGV) { |item|
print item
}
print "executed"
I would like for the
print "executed"
statement to run only if the block passed to the yield method was executed
You're going to need to init a variable outside of the block and then set it from within the block, then test that.
executed = false
yield_method(ARGV) do |item|
executed = true
# whatever else
end
print "executed" if executed
Or you can modify yield_method to return a value based on whether or not the conditions for the block to be executed were met:
def yield_method(list)
list.each do |item|
yield item
end
list.any?
end
executed = yield_method(ARGV) { ... }
print "executed" if executed
That said, needing to know whether or not a block was executed smells bad to me - I would instead make your test a test of the conditions that would permit the block to execute (as in the second form), and write your code to semantically reflect that. For example:
def process_argv(list)
list.each do |item|
yield item
end
list.length
end
args_processed = process_argv(ARGV) { ... }
print "executed" if args_processed > 0
This reflects that in this case, you care about whether there were args to process, rather than whether some block ended up being called. If the block has a side effect that you care about, you should test for that side effect directly, rather than assuming it based on block execution.

Use break keyword in the method body

I have two loops here:
loop do
prompt(messages('APR_amt', LANGUAGE))
APR_amt = Kernel.gets.chomp
if valid_number?(APR_amt)
break
else
prompt(messages('not_valid_num', LANGUAGE))
end
end
loan_duration = ''
loop do
prompt(messages('loan_duration', LANGUAGE))
loan_duration = Kernel.gets().chomp()
if valid_number?(loan_duration)
break
else
prompt(messages('not_valid_num', LANGUAGE))
end
end
This part keeps on repeating for every loop:
if valid_number?(loan_duration)
break
else
prompt(messages('not_valid_num', LANGUAGE))
end
Just different variable passing by on it.
Now what I did is that I created a method for it to shortened my codes:
def check_number(varname)
if valid_number?(varname)
break
else
prompt(messages('not_valid_num', LANGUAGE))
end
end
But this one did not work. Instead I got an error that pertains to break.
How can I create a method that will work on all of my variables?
You can raise StopIteration instead of calling break. But note that they are not equivalent:
raise StopIteration jumps of a loop created with the loop statement. The exception is not handled by loops created with statements such as while, for, or methods such as each. It has a dynamic scope, which means that it goes up the call stack until it finds the loop (which can be defined in a completely different place in the code).
break jumps out of any block. It has a lexical scope, which means that the block must enclose the break statement in the code. In your code there is no block around break (a method is not a block), and that's the reason you got an error.

Rubocop rule: Never use 'do' with multi-line 'while

I have the following code
# colours a random cell with a correct colour
def colour_random!
while true do
col, row = rand(columns), rand(rows)
cell = self[row,col]
if cell.empty? then
cell.should_be_filled? ? cell.colour!(1) : cell.colour!(0)
break
end
end
end
it's not that important what's doing, although it should pretty obvious. The point is that Rubocop gives me a warning
Never use 'do' with multi-line 'while
Why should I not do that? How should I do it then?
while is a keyword,so you don't need to pass a block. Without do..end it will work fine. The below is fine
def colour_random!
while true
col, row = rand(columns), rand(rows)
cell = self[row,col]
if cell.empty? then
cell.should_be_filled? ? cell.colour!(1) : cell.colour!(0)
break
end
end
end
while is a keyword, and if you pass a block to it, like do..end, it still works as you asked it to do, by not throwing any error, rather just a warning. But it could be dangerous if you try to pass a Proc or Method object to it, and dynamically try to convert it to a block using & keyword, as we do generally. That means
# below code will work as expected just throwing an warning.
x = 2
while x < 2 do
#code
end
But if you try to do by mistake like below
while &block # booom!! error
The reason is while is a keyword, which don't support any to_proc method to satisfy your need. So it can be dangerous.
Ruby style guide also suggested that Never use while/until condition do for multi-line while/until
I think the reason is as Nobuyoshi Nakada said in the mailing list
loop is a kernel method which takes a block. A block introduces new local variable scope.
loop do
a = 1
break
end
p a #=> causes NameError
while doesn't.
while 1
a = 1
break
end
p a #=> 1
Ruby actually has a shortcut for while true: the loop statement.
def colour_random!
loop do
col, row = rand(columns), rand(rows)
cell = self[row,col]
if cell.empty? then
cell.should_be_filled? ? cell.colour!(1) : cell.colour!(0)
break
end
end
end

Ruby, Within a While Loop, Convert String to Method Call

This is lives within a method "play" that is called once. After you enter the while loop, you stay there until you exit the process. Right now, I'm trying to use the case statement to turn user-defined strings into the variable that is passed at the end to call the next method, all within the while loop.
def play
next_action = #start # comes from an initialize function earlier in script
while true
case next_action
when beginning
next_action = beginning
when "instruct"
next_action = instructions
when "display"
next_action = display_users
else
puts "Unknown command."
next_action = display_users
end
puts "\n----------"
next_action = method(next_action).call
end
end
First problem: the case statement fails to recognize any choice but the last.
Second problem: this leads to the loop ending, jumping to the last method called, and then exiting the process.
Any help or advice is appreciated.
See if changing
next_action = #start
to:
next_action = #start.chomp
gets you any further.
you should use a state-machine instead.
See: http://railscasts.com/episodes/392-a-tour-of-state-machines

Ruby forgets local variables during a while loop?

I'm processing a record-based text file: so I'm looking for a starting string which constitutes the start of a record: there is no end-of-record marker, so I use the start of the next record to delimit the last record.
So I have built a simple program to do this, but I see something which suprises me: it looks like Ruby is forgetting local variables exist - or have I found a programming error ? [although I don't think I have : if I define the variable 'message' before my loop I don't see the error].
Here's a simplified example with example input data and error message in comments:
flag=false
# message=nil # this is will prevent the issue.
while line=gets do
if line =~/hello/ then
if flag==true then
puts "#{message}"
end
message=StringIO.new(line);
puts message
flag=true
else
message << line
end
end
# Input File example:
# hello this is a record
# this is also part of the same record
# hello this is a new record
# this is still record 2
# hello this is record 3 etc etc
#
# Error when running: [nb, first iteration is fine]
# <StringIO:0x2e845ac>
# hello
# test.rb:5: undefined local variable or method `message' for main:Object (NameError)
#
From the Ruby Programming Language:
(source: google.com)
Blocks and Variable Scope
Blocks define a new variable scope: variables created within a block exist only within that block and are undefined outside of the block. Be cautious, however; the local variables in a method are available to any blocks within that method. So if a block assigns a value to a variable that is already defined outside of the block, this does not create a new block-local variable but instead assigns a new value to the already-existing variable. Sometimes, this is exactly the behavior we want:
total = 0
data.each {|x| total += x } # Sum the elements of the data array
puts total # Print out that sum
Sometimes, however, we do not want to alter variables in the enclosing scope, but we do so inadvertently. This problem is a particular concern for block parameters in Ruby 1.8. In Ruby 1.8, if a block parameter shares the name of an existing variable, then invocations of the block simply assign a value to that existing variable rather than creating a new block-local variable. The following code, for example, is problematic because it uses the same identifier i as the block parameter for two nested blocks:
1.upto(10) do |i| # 10 rows
1.upto(10) do |i| # Each has 10 columns
print "#{i} " # Print column number
end
print " ==> Row #{i}\n" # Try to print row number, but get column number
end
Ruby 1.9 is different: block parameters are always local to their block, and invocations of the block never assign values to existing variables. If Ruby 1.9 is invoked with the -w flag, it will warn you if a block parameter has the same name as an existing variable. This helps you avoid writing code that runs differently in 1.8 and 1.9.
Ruby 1.9 is different in another important way, too. Block syntax has been extended to allow you to declare block-local variables that are guaranteed to be local, even if a variable by the same name already exists in the enclosing scope. To do this, follow the list of block parameters with a semicolon and a comma-separated list of block local variables. Here is an example:
x = y = 0 # local variables
1.upto(4) do |x;y| # x and y are local to block
# x and y "shadow" the outer variables
y = x + 1 # Use y as a scratch variable
puts y*y # Prints 4, 9, 16, 25
end
[x,y] # => [0,0]: block does not alter these
In this code, x is a block parameter: it gets a value when the block is invoked with yield. y is a block-local variable. It does not receive any value from a yield invocation, but it has the value nil until the block actually assigns some other value to it. The point of declaring these block-local variables is to guarantee that you will not inadvertently clobber the value of some existing variable. (This might happen if a block is cut-and-pasted from one method to another, for example.) If you invoke Ruby 1.9 with the -w option, it will warn you if a block-local variable shadows an existing variable.
Blocks can have more than one parameter and more than one local variable, of course. Here is a block with two parameters and three local variables:
hash.each {|key,value; i,j,k| ... }
Contrary to some of the other answers, while loops don't actually create a new scope. The problem you're seeing is more subtle.
Prerequisite knowledge: a brief scoping demo
To help show the contrast, blocks passed to a method call DO create a new scope, such that a newly assigned local variable inside the block disappears after the block exits:
### block example - provided for contrast only ###
[0].each {|e| blockvar = e }
p blockvar # NameError: undefined local variable or method
But while loops (like your case) are different, because a variable defined in the loop will persist:
arr = [0]
while arr.any?
whilevar = arr.shift
end
p whilevar # prints 0
Summary of the "problem"
The reason you get the error in your case is because the line that uses message:
puts "#{message}"
appears before any code that assigns message.
It's the same reason this code raises an error if a wasn't defined beforehand:
# Note the single (not double) equal sign.
# At first glance it looks like this should print '1',
# because the 'a' is assigned before (time-wise) the puts.
puts a if a = 1
Not Scoping, but Parsing-visibility
The so-called "problem" - i.e. local-variable visibility within a single scope - is due to ruby's parser. Since we're only considering a single scope, scoping rules have nothing to do with the problem. At the parsing stage, the parser decides at which source locations a local variable is visible, and this visibility does not change during execution.
When determining if a local variable is defined (i.e. defined? returns true) at any point in the code, the parser checks the current scope to see if any code has assigned it before, even if that code has never run (the parser can't know anything about what has or hasn't run at the parsing stage). "Before" meaning: on a line above, or on the same line and to the left-hand side.
An exercise to determine if a local is defined (i.e. visible)
Note that the following only applies to local variables, not methods. (Determining whether a method is defined in a scope is more complex, because it involves searching included modules and ancestor classes.)
A concrete way to see the local variable behavior is to open your file in a text editor. Suppose also that by repeatedly pressing the left-arrow key, you can move your cursor backward through the entire file. Now suppose you're wondering whether a certain usage of message will raise the NameError. To do this, position your cursor at the place you're using message, then keep pressing left-arrow until you either:
reach the beginning of the current scope (you must understand ruby's scoping rules in order to know when this happens)
reach code that assigns message
If you've reached an assignment before reaching the scope boundary, that means your usage of message won't raise NameError. If you don't reach any assignment, the usage will raise NameError.
Other considerations
In the case a variable assignment appears in the code but isn't run, the variable is initialized to nil:
# a is not defined before this
if false
# never executed, but makes the binding defined/visible to the else case
a = 1
else
p a # prints nil
end
While loop test case
Here's a small test case to demonstrate the oddness of the above behavior when it happens in a while loop. The affected variable here is dest_arr.
arr = [0,1]
while n = arr.shift
p( n: n, dest_arr_defined: (defined? dest_arr) )
if n == 0
dest_arr = [n]
else
dest_arr << n
p( dest_arr: dest_arr )
end
end
which outputs:
{:n=>0, :dest_arr_defined=>nil}
{:n=>1, :dest_arr_defined=>nil}
{:dest_arr=>[0, 1]}
The salient points:
The first iteration is intuitive, dest_arr is initialized to [0].
But we need to pay close attention in the second iteration (when n is 1):
At the beginning, dest_arr is undefined!
But when the code reaches the else case, dest_arr is visible again, because the interpreter sees that it was assigned 2 lines above.
Notice also, that dest_arr is only hidden at the start of the loop; its value is never lost (as evidenced by the final contents being [0, 1]).
This also explains why assigning your local before the while loop fixes the problem. The assignment doesn't need to be executed; it only needs to appear in the source code.
Lambda example
f1 = ->{ f2 }
f2 = ->{ f1 }
p f2.call()
# The following fails because the body of f1 tries to access f2 before an assignment for f2 was seen by the parser.
p f1.call() # undefined local variable or method `f2'.
Fix this by putting an f2 assignment before f1's body. Remember, the assignment doesn't actually need to be executed!
f2 = nil # Could be replaced by: if false; f2 = nil; end
f1 = ->{ f2 }
f2 = ->{ f1 }
p f2.call()
p f1.call() # ok
Method-masking gotcha
Things get really hairy if you have a local variable with the same name as a method:
def dest_arr
:whoops
end
arr = [0,1]
while n = arr.shift
p( n: n, dest_arr: dest_arr )
if n == 0
dest_arr = [n]
else
dest_arr << n
p( dest_arr: dest_arr )
end
end
Outputs:
{:n=>0, :dest_arr=>:whoops}
{:n=>1, :dest_arr=>:whoops}
{:dest_arr=>[0, 1]}
A local variable assignment in a scope will "mask"/"shadow" a method call of the same name. (You can still call the method by using explicit parentheses or an explicit receiver.) So this is similar to the previous while loop test, except that instead of becoming undefined above the assignment code, the dest_arr method becomes "unmasked"/"unshadowed" so that the method is callable w/o parentheses. But any code after the assignment will see the local variable.
Some best-practices we can derive from all this
Don't name local variables the same as method names in the same scope
Don't put the initial assignment of a local variable inside the body of a while or for loop, or anything that causes execution to jump around within a scope (calling lambdas or Continuation#call can do this too). Put the assignment before the loop.
I think this is because message is defined inside the loop. At the end of the loop iteration "message" goes out of scope. Defining "message" outside of the loop stops the variable from going out of scope at the end of each loop iteration. So I think you have the right answer.
You could output the value of message at the beginning of each loop iteration to test whether my suggestion is correct.
Why do you think this is a bug? The interpreter is telling you that message may be undefined when that particular piece of code executes.
I'm not sure why you're surprised: on line 5 (assuming that the message = nil line isn't there), you potentially use a variable that the interpreter has never heard of before. The interpreter says "What's message? It's not a method I know, it's not a variable I know, it's not a keyword..." and then you get an error message.
Here's a simpler example to show you what I mean:
while line = gets do
if line =~ /./ then
puts message # How could this work?
message = line
end
end
Which gives:
telemachus ~ $ ruby test.rb < huh
test.rb:3:in `<main>': undefined local variable or method `message' for main:Object (NameError)
Also, if you want to prepare the way for message, I would initialize it as message = '', so that it's a string (rather than nil). Otherwise, if your first line doesn't match hello, you end up tring to add line to nil - which will get you this error:
telemachus ~ $ ruby test.rb < huh
test.rb:4:in `<main>': undefined method `<<' for nil:NilClass (NoMethodError)
you can simply do this :
message=''
while line=gets do
if line =~/hello/ then
# begin a new record
p message unless message == ''
message = String.new(line)
else
message << line
end
end
# hello this is a record
# this is also part of the same record
# hello this is a new record
# this is still record 2
# hello this is record 3 etc etc

Resources