Skip to content

Instantly share code, notes, and snippets.

@alandotcom
Created September 11, 2012 18:08
Show Gist options
  • Select an option

  • Save alandotcom/3700405 to your computer and use it in GitHub Desktop.

Select an option

Save alandotcom/3700405 to your computer and use it in GitHub Desktop.
Blocks and Procs in Ruby

Originally posted as a private gist

Inspiration: What's that & doing?

Before I get started, the inspiration behind this was a piece of code written to solve a Ruby exercise: Write a method reverse_words which takes a sentence as a string and reverse each word in it.

The solution that set me on a journey of diving deeper into closures was this one, by Daniela Grossman:

	def reverse_words(str)
  		str.split(' ').map(&:reverse).join(' ')
	end

Compared to my solution:

	def reverse_words(str)
  		str.split(' ').map{|i| i.reverse }.join(' ')
	end

I wanted to know what that & meant and this lead me to learn more about closures, blocks, procs, and lambdas in Ruby.

tl;dr: The first solution is a more concise way to write the second. See: Symbol#to_proc

Blocks are closures

Blocks (do…end or {}) in Ruby are closures. This gives them special properties, mainly that they can access variables that are outside of their scope, i.e. the variables are assigned before you enter the block.

To illustrate what that means, check this out. In the following example I define a new class Calculator and define an instance variable @numbers.

	class Calculator
		def initialize(numbers)
			@numbers = numbers
		end
	
		def multiply(x)
			@numbers.map { |i| i * x }
		end
	end

Now, let's say we ran this code:

	>> calc = Calculator.new([2, 3, 4, 5])
	>> p calc.multiply(2)

Our result, unsurprisingly is:

	=> [4, 6, 8, 10]
Blocks have access to the surrounding scope

While we may take this sort of behavior for granted, something special is happening here. The { } block that is passed into the map method above is able to access the x variable. The block has access to variables defined in the surrounding scope.

The i, however, is defined within the local scope of the block. This means that only the block has access to it.

Another example shows the power of the closure property of blocks

In this example from Practicing Ruby, a Vector object is created and a method * is defined.

If you are not familiar with linear algebra or scalar multiplication: scalar multiplication.

	class Vector
  		def initialize(data)
  			@data = data
		end
		
		def *(num)
    		@data.map { |e| e * num }
  		end
	end

	>> Vector.new([1,2,3]) * 7
	=> [7, 14, 21]

We can see how the closure above is able to access num, even though it is defined outside its local scope.

Blocks and Procs

Blocks like the one in the example above are short-lived. Blocks are not objects, but you can turn them into objects and pass them around like any old object. A block that has been turned into an object is a Proc object.

For example,

	>> double = Proc.new { |i| puts 2 * i }
	>> double.call(5)
	=> 10
	
	>> puts double.class
	=> Proc
	
5-second-break

If you're struggling at this point, please try to follow along. I am not going to try to cover the entire topic of blocks and closures in depth. I think that Programming Ruby 1.9 does a very good job of this and I highly suggest reading the two chapters on this.

This is certainly one of the core concepts in Ruby. Understanding these core concepts is a pre-requisite to becoming a master Rubyist.

Now, let's go back to what started this whole discussion…

Blocks as parameters

If we go back to Daniela's solution, at this point we might be able to make sense of what is happening.

		def reverse_words(str)
  			str.split(' ').map(&:reverse).join(' ')
		end

The map method is expecting a block. If you look at my solution and several others, we've done this by explicity passing this {|i| i.reverse} block to the method.

Passing a block as a parameter to a method

To demonstrate the concept of passing a block as a parameter, I'll borrow from an example in Programming Ruby 1.9 (p.86):

	def create_block_object(&block)
		block
	end

The & prefix is telling the method to expect a block as a parameter. During runtime, when that method is called, Ruby will take the parameter passed to the method and convert it to a Proc object.

This specific method, create_block_object, will just return the block passed to it as an object.

Here it is in action:

	>> bo = create_block_object { |param| puts "You called me with #{param}" }

	>> bo.call 99
	=> You called me with 99

	>> bo.call "cat"
	=> You called me with cat

	>> puts bo.class
	=> Proc

Going back to the reverse_words solution

At this point it should be clear that map expects a block and the & is doing something special. We just learned that if you put & in front of a parameter, we are telling Ruby to convert that parameter into a Proc. We also know that Procs are just blocks converted to objects.

'&' is really just part of Ruby's syntactic sugar

The & in front of a parameter is a nice way of telling Ruby to use call the method #to_proc on the parameter. #to_proc converts an object to a Proc object (the same way we would use `to_s to convert something to a String).

The magic of Symbol#to_proc

I still haven't explained how Daniela's solution works. In her solution, she is passing a symbol, :reverse prefixed with the & and somehow the map method knows to apply the reverse method on each element that it iterates over.

The beauty comes from the implementation of Symbol#proc, which we can find in the Ruby documentation:

Symbol#proc: Returns a Proc object which respond to the given method by sym.

This means that the Proc object created will apply the method named by the symbol to each of the objects that is passed to it as a parameter. Therefore, this code

	str.split.map(&:reverse)

is exactly the same as writing

	str.split.map { |i| i.reverse }

Sources and further reading

In addition to the soures I've mentioned above, here some links that truly helped me understand the concepts I've overviewed

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