5

I'm not making sense of the following behavior (see also in this SO thread):

def def_test
  puts 'def_test.in'
  yield if block_given?
  puts 'def_test.out'
end

def_test do
  puts 'def_test ok'
end

block_test = proc do |&block|
  puts 'block_test.in'
  block.call if block
  puts 'block_test.out'
end

block_test.call do
  puts 'block_test'
end

proc_test = proc do
  puts 'proc_test.in'
  yield if block_given?
  puts 'proc_test.out'
end

proc_test.call do
  puts 'proc_test ok'
end

Output:

def_test.in
def_test ok
def_test.out
block_test.in
block_test ok
block_test.out
proc_test.in
proc_test.out

I don't mind resorting to explicitly declaring the &block variable and calling it directly, but I'd more ideally like to make some sense of why I end up needing to.

Community
  • 1
  • 1
Denis de Bernardy
  • 75,850
  • 13
  • 131
  • 154

2 Answers2

5

block_given? considers def scope, not lambda scope:

def test
  l = lambda do
    yield if block_given?
  end
  l.call
end

test { puts "In block" }
Victor Moroz
  • 9,167
  • 1
  • 19
  • 23
  • Sorry, but this is not the answer I was hoping for. My question is why doesn't `l.call { puts "In block" }` work, rather than `test { puts "In block" }`. If you edit your function to call `l.call &Proc.new` my initial impression was that it would work (since it will with a properly defined function); but it doesn't, and I'd like to understand why. – Denis de Bernardy Jul 07 '11 at 16:34
  • @Denis It depends on what you mean as "work". `yield` and `block_given?` will always work in `def` scope, `lambda` scope is transparent for both. Passing block to `lambda` doesn't change it. You can use block passed to `lambda` by `lambda do |&f|` parameter, but not by `yield` or `block_given?`. `lambda` is not the same thing ad `def`. – Victor Moroz Jul 07 '11 at 19:29
  • Yeah, it eventually "ticked", but it only did so after reading mu's answer. I see in retrospect in which ways your answer is indeed valid. It's just that, to the ruby newcomer such as myself, mu's more verbose answer makes it more clean what's happening. – Denis de Bernardy Jul 07 '11 at 21:01
4

The lambda is a closure and it seems to be capturing the block_given? and block from its outer scope. This behavior does make sense as the block is, more or less, an implied argument to the outer method; you can even capture the block in a named argument if desired:

def def_test(&block)
    frobnicate &block
end

So the block is part of the argument list even when it isn't named.

Consider this simple piece of code:

def f
    lambda do
        puts "\tbefore block"
        yield if block_given?
        puts "\tafter block"
    end
end

puts 'Calling f w/o block'
x = f; x.call
puts

puts 'Calling f w/ block'
x = f { puts "\t\tf-block" }; x.call
puts

puts 'Calling f w/o block but x with block'
x = f; x.call { puts "\t\tx-block" }
puts

puts 'Calling f w/ block and x with block'
x = f { puts "\t\tf-block" }; x.call { puts "\t\tx-block" }

This produces the following for me with 1.9.2:

Calling f w/o block
    before block
    after block

Calling f w/ block
    before block
        f-block
    after block

Calling f w/o block but x with block
    before block
    after block

Calling f w/ block and x with block
    before block
        f-block
    after block

Furthermore, Proc#call (AKA proc ===) doesn't take a block:

prc === obj → result_of_proc
Invokes the block, with obj as the block‘s parameter. It is to allow a proc object to be a target of when clause in the case statement.

Compare the first line with the documentation for Enumerable#chunk (for example):

enum.chunk {|elt| ... } → an_enumerator

The {...} indicates that chunk is documented to take a block, the lack of such notation for Proc#call indicates that Proc#call does not take a block.

This isn't exactly an authoritative answer but maybe it clears things up a little bit.

mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • I'd say it's a lot more authoritative than you might think: two hours of googling had not yielded anything more to the point. Thank you on behalf of the next new rubyists who bump into this thread! – Denis de Bernardy Jul 07 '11 at 17:20
  • @Denis: I'd call it authoritative if I could point to some official documentation that said "lambdas capture blocks along with everything else", I guess the `Proc#call` documentation combined with the "hidden `&block` parameter" argument is pretty close to that. Googling common Ruby things does to yield (ha ha) frustration but it is getting better as ruby-doc.org and apidock.com get pushed up the rankings. – mu is too short Jul 07 '11 at 17:31