Lazy evaluation?


#1

Could someone explain why this code works:

def repeat(condition)
  puts "condition: #{condition}"
  yield
  retry if not condition
end

j=0
repeat (j >= 10) do
 puts j
 j+=1
end

puts "after loop, j = #{j}"

I’d have expected repeat (j >= 10) to pass “false” into the method,
which would then yield repeatedly to the do/end block, never seeing the
(condition) part again.

martin


#2

Martin DeMello wrote:

 puts j
 j+=1
end

puts "after loop, j = #{j}"

I’d have expected repeat (j >= 10) to pass “false” into the method,
which would then yield repeatedly to the do/end block, never seeing
the (condition) part again.

retry actually reevaluates the method invocation. If you use
set_trace_func you’ll see something like this with the attached script:

[“line”, “/c/temp/ruby/retry.rb”, 8, nil, #Binding:0x100f63e8, false]
[“line”, “/c/temp/ruby/retry.rb”, 9, nil, #Binding:0x100f63a0, false]
[“c-call”, “/c/temp/ruby/retry.rb”, 9, :>=, #Binding:0x100f6250,
Fixnum]
[“c-return”, “/c/temp/ruby/retry.rb”, 9, :>=, #Binding:0x100f6130,
Fixnum]
[“call”, “/c/temp/ruby/retry.rb”, 2, :repeat, #Binding:0x100f5f08,
Object]
[“line”, “/c/temp/ruby/retry.rb”, 3, :repeat, #Binding:0x100f5ec0,
Object]
[“c-call”, “/c/temp/ruby/retry.rb”, 3, :to_s, nil, FalseClass]
[“c-return”, “/c/temp/ruby/retry.rb”, 3, :to_s, nil, FalseClass]
[“c-call”, “/c/temp/ruby/retry.rb”, 3, :puts, #Binding:0x100f5b30,
Kernel]
[“c-call”, “/c/temp/ruby/retry.rb”, 3, :write, #Binding:0x100f5a10,
IO]
condition: false[“c-return”, “/c/temp/ruby/retry.rb”, 3, :write,
#Binding:0x100f58d8, IO]
[“c-call”, “/c/temp/ruby/retry.rb”, 3, :write, #Binding:0x100f57a0,
IO]

[“c-return”, “/c/temp/ruby/retry.rb”, 3, :write, #Binding:0x100f5680,
IO]
[“c-return”, “/c/temp/ruby/retry.rb”, 3, :puts, #Binding:0x100f5560,
Kernel]
[“line”, “/c/temp/ruby/retry.rb”, 4, :repeat, #Binding:0x100f5440,
Object]
[“line”, “/c/temp/ruby/retry.rb”, 10, nil, #Binding:0x100f5230, false]
[“c-call”, “/c/temp/ruby/retry.rb”, 10, :puts, #Binding:0x100f51e8,
Kernel]
[“c-call”, “/c/temp/ruby/retry.rb”, 10, :to_s, #Binding:0x100f50c8,
Fixnum]
[“c-return”, “/c/temp/ruby/retry.rb”, 10, :to_s, #Binding:0x100f4eb8,
Fixnum]
[“c-call”, “/c/temp/ruby/retry.rb”, 10, :write, #Binding:0x100f4e70,
IO]
0[“c-return”, “/c/temp/ruby/retry.rb”, 10, :write,
#Binding:0x100f4d50,
IO]
[“c-call”, “/c/temp/ruby/retry.rb”, 10, :write, #Binding:0x100f4c30,
IO]

[“c-return”, “/c/temp/ruby/retry.rb”, 10, :write, #Binding:0x100f4b10,
IO]
[“c-return”, “/c/temp/ruby/retry.rb”, 10, :puts, #Binding:0x100f49f0,
Kernel]
[“line”, “/c/temp/ruby/retry.rb”, 11, nil, #Binding:0x100f48d0, false]
[“c-call”, “/c/temp/ruby/retry.rb”, 11, :+, #Binding:0x100f47b0,
Fixnum]
[“c-return”, “/c/temp/ruby/retry.rb”, 11, :+, #Binding:0x100f4690,
Fixnum]
[“line”, “/c/temp/ruby/retry.rb”, 5, :repeat, #Binding:0x100f4570,
Object]
[“line”, “/c/temp/ruby/retry.rb”, 5, :repeat, #Binding:0x100f4450,
Object]
[“return”, “/c/temp/ruby/retry.rb”, 3, :repeat, #Binding:0x100f4330,
Object]
[“c-call”, “/c/temp/ruby/retry.rb”, 9, :>=, #Binding:0x100f4210,
Fixnum]
[“c-return”, “/c/temp/ruby/retry.rb”, 9, :>=, #Binding:0x100f40f0,
Fixnum]
[“call”, “/c/temp/ruby/retry.rb”, 2, :repeat, #Binding:0x100f3ee0,
Object]
[“line”, “/c/temp/ruby/retry.rb”, 3, :repeat, #Binding:0x100f3e98,
Object]
[“c-call”, “/c/temp/ruby/retry.rb”, 3, :to_s, nil, FalseClass]
[“c-return”, “/c/temp/ruby/retry.rb”, 3, :to_s, nil, FalseClass]
[“c-call”, “/c/temp/ruby/retry.rb”, 3, :puts, #Binding:0x100f3b38,
Kernel]
[“c-call”, “/c/temp/ruby/retry.rb”, 3, :write, #Binding:0x100f3a18,
IO]
condition: false[“c-return”, “/c/temp/ruby/retry.rb”, 3, :write,
#Binding:0x100f38f8, IO]

You see that it goes back to line 9 reevaluating the >=. If you ask why
this is (i.e. design rationale), you probably have to ask Matz. :slight_smile: HTH

Kind regards

robert

#3

Martin DeMello wrote:

 puts j
 j+=1
end

puts "after loop, j = #{j}"

I’d have expected repeat (j >= 10) to pass “false” into the method,
which would then yield repeatedly to the do/end block, never seeing the
(condition) part again.

It’s your assumption of where the “retry” restarts execution that’s
wrong… Essentially the retry in “repeat” will restart execution right
before the call to repeat, not at the start of the “repeat” method,
thus evaluating the arguments again.

You can see that by changing your code so that there’s an observable
side effect of the reevaluation:

def cond©
puts “Reevaluating the condition”
c
end

def repeat(condition)
puts “condition: #{condition}”
yield
retry if not condition
end

j=0
repeat (cond(j >= 10)) do
puts j
j+=1
end

puts “after loop, j = #{j}”

It surprised me too, but it is much more useful that way.

Vidar


#4

removed_email_address@domain.invalid removed_email_address@domain.invalid wrote:

It’s your assumption of where the “retry” restarts execution that’s
wrong… Essentially the retry in “repeat” will restart execution right
before the call to repeat, not at the start of the “repeat” method,
thus evaluating the arguments again.

You can see that by changing your code so that there’s an observable
side effect of the reevaluation:

This seems wrong to me, for precisely the reason you cite - it means
that I can never do something like object.foo(ary.pop), without
physically reading the source of #foo to see whether it calls retry
anywhere.

martin


#5

Martin DeMello wrote:

This seems wrong to me, for precisely the reason you cite - it means
that I can never do something like object.foo(ary.pop), without
physically reading the source of #foo to see whether it calls retry
anywhere.

Run the code I posted and see. Or see the reply with an example using
tracing posted about the same time as my reply. Or to take any example
using pop:

def repeat(cond)
return if !cond
yield
retry
end

ary = [1,2,3,4,5]

repeat(ary.pop) do
p ary.size
end

In any case, though, regardless of what method you call you would
presumably want to know what it does before you use it, whether by
reading the source or reading documentation, or relying on the method
to be sanely named.

I just don’t see how this behaviour makes any difference in terms of
what surprises a badly documented and/or badly named method might have
in store for you. After all, nothing stops anyone from doing all kinds
of other completely unexpected things to your environment in some
innocent sounding method - “retry” doesn’t exactly add a lot to their
arsenal, and it makes it a lot easier to create your own control
structures.

Vidar


#6

Martin DeMello wrote:

This seems wrong to me, for precisely the reason you cite - it means
that I can never do something like object.foo(ary.pop), without
physically reading the source of #foo to see whether it calls retry
anywhere.

That’s like saying you have to read the source of every function before
using it, just to make sure it’s not going to start deleting things from
your hard drive. Retry is a design tool that is appropriate in specific
situations; it is not inserted at random in library functions.

It should be safe to trust methods not to ‘retry’ unless you have reason
to believe otherwise.


#7

Martin DeMello wrote:

This seems wrong to me, for precisely the reason you cite - it means
that I can never do something like object.foo(ary.pop), without
physically reading the source of #foo to see whether it calls retry
anywhere.

I never thought of it that way. I always thought of it in the context
of reevaluating an expression whose value might have changed – but
side effects never occurred to me.

Hal


#8

Martin DeMello wrote:

 puts j
 j+=1
end

puts "after loop, j = #{j}"

I’d have expected repeat (j >= 10) to pass “false” into the method,
which would then yield repeatedly to the do/end block, never seeing the
(condition) part again.

martin

What confused me about this is that retry has two meanings, one in the
context of a loop or block, and another in the context of begin…end
(inside a rescue clause). From PickAxe v2:

break terminates the immediately enclosing loop–control resumes at
the statement following the block. redo repeats the loop from the
start, but without reevaluating the condition or fetching the next
element (in an iterator). The next keyword skips to the end of the
loop, effectively starting the next iteration. retry restarts the
loop, reevaluating the condition. [p.330]
^^^^^^^^^^^^^^^^^^^^^^^^^^
Apparently, this includes the arguments of the method, in
the case of an iterator method called with a block.

The retry statement can be used within a rescue clause to restart the
enclosing begin/end block from the beginning. [p. 347]

Which interpretation is used can depend on whether a block is present:

$ cat thrice.rb
def thrice(x)
@count = 0 if !@count || @count >= 3
@count += 1
retry if @count < 3
end

thrice puts(“foo”) do end
thrice puts(“foo”)

$ ruby thrice.rb
foo
foo
foo
foo
thrice.rb:4:in `thrice’: retry outside of rescue clause (LocalJumpError)
from thrice.rb:8

So there’s no need to worry about object.foo(ary.pop) reevaluating its
argument. That can only happen if a block is present, in which case the
caller should be aware of any “control structure” semantics that #foo
might have.