Does adding a block to Object.send pass it to the called method?

TL;DR: Yes, the send method will pass the closure to the method it calls.

Under the hood, a closure supplied to a method call implicitly gets passed to the method as an additional argument (albeit a special one). You can use the closure explicitly with call ...

> def do_it(&block)
>   block.call
> end
 => :do_it 

> do_it { puts 'apples' }
apples
 => nil

... or implicitly with yield.

> def do_it_with_yield
>   yield if block_given?
> end
 => :do_it_with_yield

> do_it_with_yield { puts 'bananas' }
bananas
 => nil

You can pass closures to any method, including :send, :public_send, etc., though it is up to the method called to use the closure or not.

In the case of send et al, they dutifully pass the closure along as we would hope and expect.

> self.public_send(:do_it) { puts 'cherries' }
cherries
 => nil

> self.send(:do_it_with_yield) { puts 'dates' }
dates
 => nil 

This is important when implementing the method_missing method, for example.

def method_missing(method_name, *args, &block)
  puts "Delegating #{method_name} to api handler"
  my_api_handler.send(method_name, *args, &block)
end

References:

  • https://scotch.io/tutorials/understanding-ruby-closures
  • https://medium.com/rubycademy/the-yield-keyword-603a850b8921
  • https://www.leighhalliday.com/ruby-metaprogramming-method-missing

Yes, it will. Inside the method you can check it with block_given? and call the block with yield

Code

class Foo
  def bar
    puts "before yield"
    yield if block_given?
    puts "after yield"
  end
end

f = Foo.new
f.send(:bar)
puts ""
f.send(:bar) { puts "yield"}

Output

before yield
after yield

before yield
yield
after yield

The documentation is a bit unclear on this:

send(symbol [, args...]) → obj

Invokes the method identified by symbol, passing it any arguments specified.

But note the any arguments specified part. The block that you give to a method is really a funny type of implicit argument so that you can do things like:

def m(&b)
  @a.each(&b)
end
m { |e| puts e }

to pass the block around as a Proc instance. However, you can also do this:

def m
  yield
end
m { puts 'pancakes' }

so the block is special as far as the argument list is concerned but the block still behaves as an argument even if it is sometimes implicit.

Given the above "block is sort of an argument" rambling and the importance of blocks in Ruby, it would be reasonable for send to pass the block through. You can also try it but you have to careful about accidental and undocumented behavior with the "try it" approach:

class C
  def m
    yield
  end
end
o = C.new
o.send(:m) { puts 'pancakes' }
# "pancakes" comes out

Yes. Consider the following:

class A
  def explicit(&b); b; end
  def implicit; yield "hello"; end
end

>> A.new.send(:explicit) { }
=> #<Proc:0x0000000000000000@(irb):19>
>> A.new.send(:implicit) { |greeting| puts greeting }
hello
=> nil

Hope this helps!

Tags:

Ruby