Ruby blocks
One powerful mechanism for abstraction in Ruby is the block.
Blocks, are ways to pass around code. This is likely a new concept, because in Java, code is treated as a second class citizen: methods can’t accept code as an argument.
In Ruby, this practice is relatively common:
1 x = [1,2,3]
2 x.each { |x| puts x }The .each method of the Array class accpets a block as parameter. It then calls that block with each element of the array.
An analogy to objects
If you’re used to callbacks in Java, blocks are similar. In object oriented languages, methods can’t accept code as arguments, but they can accept objects as arguments. We could write a method in Java that looked something like this:
1 interface Callback {
2 void call();
3 }
4
5 public void doForEach(int[] e, Callback o) {
6 for each i in e {
7 o.callback(i);
8 }
9 }
10
11 class PrintToScreenCallback implements Callback {
12 void call(int e) {
13 print(e);
14 }
15 }In Java, code has to be wrapped in an Object. So for each thing we wanted to pass to doForEach, we’d have to make a new instance of Callback that did the thing we wanted.
This is very similar to what’s happening in the Ruby world — at a foundational level — but we don’t need to go to the burden of writing all of the heavyweight syntax.
Digging into .each
The most common usage of blocks in Ruby is iterators. Let’s look at the documentation for Ruby’s Array.each:
each { |item| block } → ary
each → Enumerator
Calls the given block once for each element in self, passing that element as a parameter.An Enumerator is returned if no block is given.
a = [ "a", "b", "c" ]
a.each {|x| print x, " -- " }
produces:
a -- b -- c --This means that the return type of .each changes depending on its arguments. If you pass in nothing, it gives you an Enumerator. If you pass in a block, it calls that with each constituent item of the array.
Block syntax
Blocks have the following syntax:
[1,2].each { |x| ... }
[1,2].each begin |x| ... endThe two forms are equivalent. The style I suggest is that you use {} for one-liners, and to use begin/end for blocks that go on for more than a line.
Enumerators
An enumerator is an object that is waiting to call a piece of code.
Look at the following:
1 2.0.0-p0 :004 > x = [1,2]
2 => [1, 2]
3 2.0.0-p0 :006 > y = x.each
4 => #<Enumerator: [1, 2]:each>
5 2.0.0-p0 :007 > y.each { |x| puts x }
6 1
7 2
8 => [1, 2]Notice that x.each gives me back an object of type Enumerator. An enumerator is a suspended computation. When you call it’s .each method with a block, it will fire off that block for each of the things the enumerator contains.
Enumerators are a method of suspended computation. By calling x = [1,2].each, and storing the result, we get a computation that will execute when we call x.each { |x| ... }. This notion of suspended computation is called a thunk. We’ll come back to this concept later in the semester, so it’s helpful to remember this simple examlpe now.
Taking blocks as an argument
You can write code that takes blocks as arguments too! Here’s a method that takes a block as a parameter:
1 def example
2 yield(2)
3 endThe thing that’s strange about this is that the block name isn’t bound as a formal parameter to the method in its definition. Nonetheless, we can call the method with our block:
1 example { |x| x + 2 }
2 # Gives us 4The way to think about this is that, implicitly, yield says “take whatever code was passed to this method, and put it right here!” If we were to do that in this example we’d get:
1 def example
2 ({ |x| x + 2 }) (2)
3 end(Note that I’ve just textually substituted our block for the yield expression.)
The way we evaluate this is to take the block body and substitute 2 for x everywhere, then strip off the block, so we’re left with:
1 def example
2 2 + 2
3 endWhich evaluates to 4, of course. But the powerful thing is that we could pass in any expression to this block. We can even do this:
1 def h
2 y = 23
3 example { |x| y = x + y }
4 y
5 endNow, if we call h, we get the result 25! If this isn’t surprising to you, do what we did before:
1 def example
2 yield(2)
3 end
4 # Becomes...
5 def example
6 { |x| y = x + y } (2)
7 end
8 # Becomes...
9 def example
10 y = 2 + y
11 endBut this is strange! Because y never appears anywhere in the method example. So what y is it referring to?
Well, think about it this way: when the Ruby interpreter goes to evaluate h, it first sees line 2, and creates a fresh new Fixnum object that stores 23. It stores it at some location (in memory) 0xABCD (let’s pretend). y points at that object. Next it sees the yield expression, and it sees a block. It takes the variables that aren’t x and looks them up in the current environment. In this case, y points at 0xABCD, holding the number 23. If we passed that block into example, example wouldn’t be able to look up y! So h takes the block { |x| y = x + y } and transforms it to this:
{ |x| 0xABCD = x + 0xABCD }
Now, only the variable x is unspecified in the block. But example is going to plug that hole for us. “Plugging in” for the variable x is just like you did in high school algebra: you substituted for points on a function. It’s the same operation here!
If it’s hard, just imagine if I handed you a function in high school that said f(x) = x. You’d tell me it was the identity function. Now if I said f(x) = x + y, you’d say “well, I’m not sure, what’s the value of y?” The reason you’re confused is that I handed you an expression with unbound variables. An expression that has no unbound variables is called closed. The operation of taking an expression with unbound variables, and substituting (like we did with 0xABCD) is called closing. You can think of it as “plugging the holes” before we pass it off to example for it to do its work.
This is the first instance of what’s called a closure. We call blocks like: { |x| 0xABCD = x + 0xABCD } and { |x,y| 0xABCD = x + y + 0xABCD } closed. Because blocks can close over variables like y, Ruby programmers sometimes refer to blocks as closures.
Closures are an extremely powerful and foundational concept in programming languages that are going to show up later. It turns out that what we just did was an example of (beta) reduction of a lambda expression. This is a fancy term for saying “we reduce the function after substituting its arguments.”