Procs and Lambdas

Blocks are syntactic structures in Ruby; they are not objects, and cannot be manipulated as objects. It is possible, however, to create an object that represents a block. Depending on how the object is created, it is called a proc or a lambda. Procs have block-like behavior and lambdas have method-like behavior. Both, however, are instances of class Proc.

The subsections that follow explain:

  • How to create Proc objects in both proc and lambda forms

  • How to invoke Proc objects

  • How to determine how many arguments a Proc expects

  • How to determine if two Proc objects are the same

  • How procs and lambdas differ from each other

Creating Procs

We’ve already seen one way to create a Proc object: by associating a block with a method that is defined with an ampersand-prefixed block argument. There is nothing preventing such a method from returning the Proc object for use outside the method:

# This method creates a proc from a block
def makeproc(&p)  # Convert associated block to a Proc and store in p
  p               # Return the Proc object
end

With a makeproc method like this defined, we can create a Proc object for ourselves:

adder = makeproc {|x,y| x+y }

The variable adder now refers to a Proc object. Proc objects created in this way are procs, not lambdas. All Proc objects have a call method that, when invoked, runs the code contained by the block from which the proc was created. For example:

sum = adder.call(2,2)  # => 4

In addition to being invoked, Proc objects can be passed to methods, stored in data structures and otherwise manipulated like any other Ruby object.

As well as creating procs by method invocation, there are three methods that create Proc objects (both procs and lambdas) in Ruby. These methods are commonly used, and it is not actually necessary to define a makeproc method like the one shown earlier. In addition to these Proc-creation methods, Ruby 1.9 also supports a new literal syntax for defining lambdas. The subsections that follow discuss the methods Proc.new, lambda, and proc, and also explain the Ruby 1.9 lambda literal syntax.

Proc.new

We’ve already seen Proc.new used in some of the previous examples in this chapter. This is the normal new method that most classes support, and it’s the most obvious way to create a new instance of the Proc class. Proc.new expects no arguments, and returns a Proc object that is a proc (not a lambda). When you invoke Proc.new with an associated block, it returns a proc that represents the block. For example:

p = Proc.new {|x,y| x+y }

If Proc.new is invoked without a block from within a method that does have an associated block, then it returns a proc representing the block associated with the containing method. Using Proc.new in this way provides an alternative to using an ampersand-prefixed block argument in a method definition. The following two methods are equivalent, for example:

def invoke(&b)     def invoke
  b.call             Proc.new.call
end                end

Kernel.lambda

Another technique for creating Proc objects is with the lambda method. lambda is a method of the Kernel module, so it behaves like a global function. As its name suggests, the Proc object returned by this method is a lambda rather than a proc. lambda expects no arguments, but there must be a block associated with the invocation:

is_positive = lambda {|x| x > 0 }

Kernel.proc

In Ruby 1.8, the global proc method is a synonym for lambda. Despite its name, it returns a lambda, not a proc. Ruby 1.9 fixes this; in that version of the language, proc is a synonym for Proc.new.

Because of this ambiguity, you should never use proc in Ruby 1.8 code. The behavior of your code might change if the interpreter was upgraded to a newer version. If you are using Ruby 1.9 code and are confident that it will never be run with a Ruby 1.8 interpreter, you can safely use proc as a more elegant shorthand for Proc.new.

Lambda Literals

Ruby 1.9 supports an entirely new syntax for defining lambdas as literals. We’ll begin with a Ruby 1.8 lambda, created with the lambda method:

succ = lambda {|x| x+1}

In Ruby 1.9, we can convert this to a literal as follows:

  • Replace the method name lambda with the punctuation ->.

  • Move the list of arguments outside of and just before the curly braces.

  • Change the argument list delimiters from || to ().

With these changes, we get a Ruby 1.9 lambda literal:

succ = ->(x){ x+1 }

succ now holds a Proc object, which we can use just like any other:

succ.call(2)    # => 3

The introduction of this syntax into Ruby was controversial, and it takes some getting used to. Note that the arrow characters -> are different from those used in hash literals. If you squint at the arrow, you may be able to convince yourself that the greater-than sign is the Greek letter lambda (λ) with its right-hand leg chopped off, turned into a hyphen and moved to the left!

As with blocks in Ruby 1.9, the argument list of a lambda literal may include the declaration of block-local variables that are guaranteed not to overwrite variables with the same name in the enclosing scope. Simply follow the parameter list with a semicolon and a list of local variables:

# This lambda takes 2 args and declares 3 local vars
f = ->(x,y; i,j,k) { ... }

Lambda literals can be declared with argument defaults, just as methods can:

zoom = ->(x,y,factor=2) { [x*factor, y*factor] }

The parentheses around the argument list of a lambda literal are only required if the argument list includes a semicolon and block-local variable names. Otherwise parentheses may be omitted, resulting in a more compact syntax:

succ = ->x { x+1 }
zoom = ->x,y,factor=2 { [x*factor, y*factor] }

If the argument list of a lambda literal has parentheses around it, there must not be a space between the -> and the open parenthesis.

Lambda parameters and local variables are optional, of course, and a lambda literal can omit this altogether. The minimal lambda, which takes no arguments and returns nil, is the following:

->{}

One benefit of this new syntax is its succinctness. It can be helpful when you want to pass a lambda as an argument to a method or to another lambda:

def compose(f,g)            # Compose 2 lambdas 
  ->(x) { f.call(g.call(x)) }
end
succOfSquare = compose(->x{x+1}, ->x{x*x})
succOfSquare.call(4)        # => 17: Computes (4*4)+1

Lambda literals create Proc objects and are not the same thing as blocks. If you want to pass a lambda literal to a method that expects a block, prefix the literal with &, just as you would with any other Proc object. Here is how we might sort an array of numbers into descending order using both a block and a lambda literal:

data.sort {|a,b| b-a }   # The block version
data.sort &->(a,b){ b-a } # The lambda literal version

In this case, as you can see, regular block syntax is simpler.

Invoking Procs and Lambdas

Procs and lambdas are objects, not methods, and they cannot be invoked in the same way that methods are. If p refers to a Proc object, you cannot invoke p as a method. But because p is an object, you can invoke a method of p. We’ve already mentioned that the Proc class defines a method named call. Invoking this method executes the code in the original block. The arguments you pass to the call method become arguments to the block, and the return value of the block becomes the return value of the call method:

f = Proc.new {|x,y| 1.0/(1.0/x + 1.0/y) }
z = f.call(x,y)

The Proc class also defines the array access operator to work the same way as call. This means that you can invoke a proc or lambda using a syntax that is like method invocation, where parentheses have been replaced with square brackets. The proc invocation above, for example, could be replaced with this code:

z = f[x,y]

Ruby 1.9 offers an additional way to invoke a Proc object; as an alternative to square brackets, you can use parentheses prefixed with a period:

z = f.(x,y)

.() looks like a method invocation missing the method name. This is not an operator that can be defined, but rather is syntactic-sugar that invokes the call method. It can be used with any object that defines a call method and is not limited to Proc objects.

Ruby 1.9 adds a curry method to the Proc class. Calling this method returns a curried version of a proc or lambda. When a curried proc or lambda is invoked with insufficient arguments it returns a new Proc object (also curried) with the given arguments applied. Currying is a common technique in the functional programming paradigm:

product = ->(x,y){ x*y }  # Define a lambda
triple = product.curry[3] # Curry it, then specify the first argument
[triple[10],triple[20]]   # => [30,60]: 
lambda {|w,x,y,z| w+x+y+z}.curry[1][2,3][4] # => 10
        

The Arity of a Proc

The arity of a proc or lambda is the number of arguments it expects. (The word is derived from the “ary” suffix of unary, binary, ternary, etc.) Proc objects have an arity method that returns the number of arguments they expect. For example:

lambda{||}.arity        # => 0. No arguments expected
lambda{|x| x}.arity     # => 1. One argument expected
lambda{|x,y| x+y}.arity # => 2. Two arguments expected

The notion of arity gets confusing when a Proc accepts an arbitrary number of arguments in an *-prefixed final argument. When a Proc allows optional arguments, the arity method returns a negative number of the form -n-1. A return value of this form indicates that the Proc requires n arguments, but it may optionally take additional arguments as well. -n-1 is known as the one’s-complement of n, and you can invert it with the ~ operator. So if arity returns a negative number m, then ~m (or -m-1) gives you the number of required arguments:

lambda {|*args|}.arity        # => -1.  ~-1 = -(-1)-1 = 0 arguments required
lambda {|first, *rest|}.arity # => -2.  ~-2 = -(-2)-1 = 1 argument required

There is one final wrinkle to the arity method. In Ruby 1.8, a Proc declared without any argument clause at all (that is, without any || characters) may be invoked with any number of arguments (and these arguments are ignored). The arity method returns –1 to indicate that there are no required arguments. This has changed in Ruby 1.9: a Proc declared like this has an arity of 0. If it is a lambda, then it is an error to invoke it with any arguments:

puts lambda {}.arity  # –1 in Ruby 1.8; 0 in Ruby 1.9

Proc Equality

The Proc class defines an == method to determine whether two Proc objects are equal. It is important to understand, however, that merely having the same source code is not enough to make two procs or lambdas equal to each other:

lambda {|x| x*x } == lambda {|x| x*x }  # => false

The == method only returns true if one Proc is a clone or duplicate of the other:

p = lambda {|x| x*x }
q = p.dup
p == q                      # => true: the two procs are equal
p.object_id == q.object_id  # => false: they are not the same object

How Lambdas Differ from Procs

A proc is the object form of a block, and it behaves like a block. A lambda has slightly modified behavior and behaves more like a method than a block. Calling a proc is like yielding to a block, whereas calling a lambda is like invoking a method. In Ruby 1.9, you can determine whether a Proc object is a proc or a lambda with the instance method lambda?. This predicate returns true for lambdas and false for procs. The subsections that follow explain the differences between procs and lambdas in detail.

Return in blocks, procs, and lambdas

Recall from Chapter 5 that the return statement returns from the lexically enclosing method, even when the statement is contained within a block. The return statement in a block does not just return from the block to the invoking iterator, it returns from the method that invoked the iterator. For example:

def test
  puts "entering method"
  1.times { puts "entering block"; return }  # Makes test method return
  puts "exiting method"  # This line is never executed
end
test

A proc is like a block, so if you call a proc that executes a return statement, it attempts to return from the method that encloses the block that was converted to the proc. For example:

def test
  puts "entering method"
  p = Proc.new { puts "entering proc"; return } 
  p.call                 # Invoking the proc makes method return
  puts "exiting method"  # This line is never executed
end
test

Using a return statement in a proc is tricky, however, because procs are often passed around between methods. By the time a proc is invoked, the lexically enclosing method may already have returned:

def procBuilder(message)            # Create and return a proc
  Proc.new { puts message; return } # return returns from procBuilder
  # but procBuilder has already returned here!
end

def test
  puts "entering method"
  p = procBuilder("entering proc")
  p.call                 # Prints "entering proc" and raises LocalJumpError!
  puts "exiting method"  # This line is never executed
end
test

By converting a block into an object, we are able to pass that object around and use it “out of context.” If we do this, we run the risk of returning from a method that has already returned, as was the case here. When this happens, Ruby raises a LocalJumpError.

The fix for this contrived example is to remove the unnecessary return statement, of course. But a return statement is not always unnecessary, and another fix is to use a lambda instead of a proc. As we said earlier, lambdas work more like methods than blocks. A return statement in a lambda, therefore, returns from the lambda itself, not from the method that surrounds the creation site of the lambda:

def test
  puts "entering method"
  p = lambda { puts "entering lambda"; return } 
  p.call                 # Invoking the lambda does not make the method return
  puts "exiting method"  # This line *is* executed now
end
test

The fact that return in a lambda only returns from the lambda itself means that we never have to worry about LocalJumpError:

def lambdaBuilder(message)        # Create and return a lambda
  lambda { puts message; return } # return returns from the lambda
end

def test
  puts "entering method"
  l = lambdaBuilder("entering lambda")
  l.call                 # Prints "entering lambda" 
  puts "exiting method"  # This line is executed
end
test

Break in blocks, procs and lambdas

Figure 5-3 illustrated the behavior of the break statement in a block; it causes the block to return to its iterator and the iterator to return to the method that invoked it. Because procs work like blocks, we expect break to do the same thing in a proc. We can’t easily test this, however. When we create a proc with Proc.new, Proc.new is the iterator that break would return from. And by the time we can invoke the proc object, the iterator has already returned. So it never makes sense to have a top-level break statement in a proc created with Proc.new:

def test
  puts "entering test method"
  proc = Proc.new { puts "entering proc"; break }
  proc.call                    # LocalJumpError: iterator has already returned
  puts "exiting test method"
end
test

If we create a proc object with an & argument to the iterator method, then we can invoke it and make the iterator return:

def iterator(&proc)
  puts "entering iterator"
  proc.call  # invoke the proc
  puts "exiting iterator"   # Never executed if the proc breaks
end

def test
  iterator { puts "entering proc"; break }
end
test

Lambdas are method-like, so putting a break statement at the top-level of a lambda, without an enclosing loop or iteration to break out of, doesn’t actually make any sense! We might expect the following code to fail because there is nothing to break out of in the lambda. In fact, the top-level break just acts like a return:

def test
  puts "entering test method"
  lambda = lambda { puts "entering lambda"; break; puts "exiting lambda" }
  lambda.call  
  puts "exiting test method"
end
test

Other control-flow statements

A top-level next statement works the same in a block, proc, or lambda: it causes the yield statement or call method that invoked the block, proc, or lambda to return. If next is followed by an expression, then the value of that expression becomes the return value of the block, proc, or lambda.

redo also works the same in procs and lambdas: it transfers control back to the beginning of the proc or lambda.

retry is never allowed in procs or lambdas: using it always results in a LocalJumpError.

raise behaves the same in blocks, procs, and lambdas. Exceptions always propagate up the call stack. If a block, proc, or lambda raises an exception and there is no local rescue clause, the exception first propagates to the method that invoked the block with yield or that invoked the proc or lambda with call.

Argument passing to procs and lambdas

Invoking a block with yield is similar to, but not the same as, invoking a method. There are differences in the way argument values in the invocation are assigned to the argument variables declared in the block or method. The yield statement uses yield semantics, whereas method invocation uses invocation semantics. Yield semantics are similar to parallel assignment and are described in Passing Arguments to a Block. As you might expect, invoking a proc uses yield semantics and invoking a lambda uses invocation semantics:

p = Proc.new {|x,y| print x,y }
p.call(1)       # x,y=1:     nil used for missing rvalue:  Prints 1nil
p.call(1,2)     # x,y=1,2:   2 lvalues, 2 rvalues:         Prints 12
p.call(1,2,3)   # x,y=1,2,3: extra rvalue discarded:       Prints 12
p.call([1,2])   # x,y=[1,2]: array automatically unpacked: Prints 12

This code demonstrates that the call method of a proc handles the arguments it receives flexibly: silently discarding extras, silently adding nil for omitted arguments, and even unpacking arrays. (Or, not demonstrated here, packing multiple arguments into a single array when the proc expects only a single argument.)

Lambdas are not flexible in this way; like methods, they must be invoked with precisely the number of arguments they are declared with:

l = lambda {|x,y| print x,y }
l.call(1,2)     # This works
l.call(1)       # Wrong number of arguments
l.call(1,2,3)   # Wrong number of arguments
l.call([1,2])   # Wrong number of arguments
l.call(*[1,2])  # Works: explicit splat to unpack the array
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.71.21