Skip to content

Instantly share code, notes, and snippets.

@dkinzer
Last active November 3, 2017 18:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dkinzer/d3fed3b34b4456382f31d4b9916f3d9f to your computer and use it in GitHub Desktop.
Save dkinzer/d3fed3b34b4456382f31d4b9916f3d9f to your computer and use it in GitHub Desktop.

To Block or Not to Block

Defining a Ruby method that can accept either a block or a Proc as an argument1


The problem:

Recently I was trying to refactor some code to make it more testable. The code I wanted to refactor was in a block. But, I wanted to be able to test the block code independently from the method that was using the block.

def dsl_method
  yield
end

dsl_method { testable_method  }

One obvious way to do this (shown in the above example) would have been to refactor the block code I wanted to test into a testable method or procedure and call that testable method from within the block. That would have been a reasonable (albeit verbose) approach. However, what I really wanted to be able to do was to replace the block altogether with a procedure that I could pass in lieu of the block. For example:

dsl_method testable_method

As it turns out the DSL method had been implemented in such a way as to allow either passing a procedure or a block using a special argument name prefix, &:

def block_or_proc(&block)
  block
end

a = block_or_proc { puts "hello block" }
a.call
# Outputs: "hello block"

p = Proc.new { puts "hello proc" }
b = block_method(&p).call
b.call
# Outputs: "hello proc"

Using this prefix coerces a passed in block into a procedure. That works, sort of. But as you can see above we are forced to prefix & to the procedure name when we pass it into block_or_proc or we'll get the following error thrown:

wrong number of arguments (given 1, expected 0) (ArgumentError)

Is there a way that I can pass a procedure or block into the method without needing to remember some extraneous voodoo?

The solution:

Indeed we can. If we take over the coercion of the block into a procedure, then it becomes possible for us to pass either a block or a procedure without needing to prefix & to the procedure name.

def block_or_proc(block = Proc.new {|*args|})
  if block_given?
    block = Proc.new { |*args| yield args }
  end

  block
end

# Using blocks with no additional args.
a = block_or_proc { puts "hello" }
a.call
# Outputs: "hello"

# Using procs with no addtional args.
p = Proc.new { puts "goodbye "}
b = block_or_proc(p)
b.call
# Outputs: "goodbye"

# Using procs with args.
p = Proc.new { |x, y| x + y }
c = block_or_proc(p)
puts c.call(3, 4)
# Outputs: 7

# Using procs with more args.
d = block_or_proc { |x, y, z| x + y + z }
puts d.call(3, 4, 1)
# Outputs: 8

# Not passing anything.
e = block_or_proc
puts (e.call).class
# Outpus: NilClass

# Still works with `&`?
f = block_or_proc(&p)
puts f.call(4, 5)
# Outputs: 9

The explanation:

There's not too much going on there, and from the examples we can see that this method works as expected. But, let's go ahead and break it down anyway:

First things first.

def block_or_proc(block = Proc.new {|*args|})

OK, this first bit is about ensuring that if nothing is passed to our method that we generate a procedure that takes a variable amount of arguments and does nothing. A procedure that does nothing is a good idea to have around in case there is a chance that you might try to use nil as if it where a procedure. We test against this possibility in the example where we pass nothing.

# Not passing anything (NilClass)
e = block_or_proc
puts (e.call).class

Of course, if we had not protected against this possibility e.call would surely throw an error of trying to call a method on a nil.2

Where the magic happens.

The next bit is where the magic really happens. First off, we check to see if a block was passed (after all that's the whole point)

  if block_given?

It's nice that Ruby gives us an easy way to know if a block has been passed to our method (specially considering that all methods can accept a block by default).

But there is something a little less obvious going on in the next line:

    block = Proc.new { |*args| yield args }

At this point the value of block is either the default instance of block (the argument, i.e. block = Proc.new {|*args|}) or something all together different if you were silly enough to call the method with both an argument and a block (But let's assume you are using this method as intended).

If you did call the method with a block (not the argument but the syntactical version) then whatever block is will get replaced by

Proc.new { |*args| yield args }`

The outer shell of this is the easiest (most obvious?) to explain. We are attempting to create an instance of Proc that takes a variable number of arguments, *args, and does something with it:

Proc.new { |*args| ... }

This is very similar to the default case, except that the Proc may actually serve a purpose other than returning nil when called (assuming the passed in block does something useful).

So now to explain the itsy bitsy slight of hand. Assume that instead of block = Proc.new { |*args| yield args }

I were to have used:

    block = yield args

"Foul! Where does args come from?!" you would exclaim. And you would be right, but let's assume that it's available... Maybe the new method signature is now def block_or_proc *args or something.

But aside from that (and maybe that block is a dumb variable name here) yield args is simply the magic syntax that Ruby supplies for us to communicate with the amorphous block passed into our method. When we yield to this amorphous thing, it gets executed either by passing nil into it or whatever arguments you like (in this specific case it's args).

So there is a little bit of magic there. And I think that magic or ambiguity mostly arises from having to work with a value that is essentially anonymous . But, we know it's there since we checked for it with block_given? (remember).

Now, for the real magic (drum rolls please); the real magic comes from combining that little bit of magic we just mentioned with the power of the closure. Yes, I have said it. The closure rears it's interesting, beautiful and powerful head yet again.

For what is Proc.new {} but a closure when invoked from inside a method, and what is a closure but an entity that continues to have access to the scope it was invoked from even when no longer part of that scope; a scope that may or may not include say a block passed to the method where the closure was originally created.

And there you have it, folks:

    block = Proc.new { |*args| yield args }

block is a closure with access to the block that was passed to the method where it was created. And one day if and when we send a call to that closure, it will yield to the block that was passed to the method where it was created. The closure will be able to yield to the original block wherever it finds itself (original or new scope)

Why not lambdas?

You might be asking yourself, why all this focus on procedures; what about lambdas?

Indeed, if you look under the hood you will see that even the default behavior that Ruby provides is to coerce blocks into regular old Procs.

def block_method(&block)
  block.inspect
end

block_method {}

# Returns: "#<Proc:0x00007f868a1f5348@(pry):8>"

The reason Ruby chooses to coerce its blocks into a procedures by default is that it's a more general solution. In fact, Ruby lambdas are a specific type of Proc that does not allow an arbitrary number of arguments3. Lambdas are very strict about the argument arity.

Let's say we have the following code:

def block_to_lambda(block = lambda {})
  if block_given?
    block = lambda { yield }
  end

  puts block.inspect
end

block_to_lambda {}
# Returns: #<Proc:0x00007fceeba0ed50@test_block.rb:49 (lambda)>

Above you can see that we have successfully coerced a block into a lambda. But try passing a block that takes any number of arguments (other than no arguments at all) and you will get an error.

So it is possible to do this, but it needs to be done on a case by case basis since it's more restrictive than using a regular Proc.

In Summary:

When rubyists speak about the expressiveness of their language, in part (I believe) they are referring to this quality of ruby to not be tied down to one and only one syntactical way of expressing an idea.

Indeed, in ruby it is possible to create a method that allows it's user to invoke it either with a passed in procedure or a block. And as we showed it is possible to pass a procedure to such a method without requiring any new or weird syntax.


1: A more pretentious version of this blog post is available at Medium.

2: Guarding against the use of nil as a procedure is not absolutely necessary, and in fact doing so is one of the reasons a PR I submitted with this change was rejected.

3: The other major difference between the regular procedure and the lambda is the behavior on return. Since lambdas return whatever value is passed to return, they are a bit easier to reason about (imho) and in some respect behave more functionally than procedures do. There are clearly cases where that quality matters and it’s a good idea to be able to use lambdas instead of procedures in specific cases (not elaborated on here)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment