Skip to content
academia.sh

Lesson 10 / 12

Procs and Lambdas

For procs and lambdas, the two ways of turning a block into an object, a class is measured answering the call message, and the difference is shown to lie in argument checking and in the lambda? state.

Published Updated

In this lesson

  • Assigns a block to a variable as a proc or a lambda and calls it with call
  • Measures the argument-checking difference between a proc and a lambda
  • Asks what kind of object something is with lambda?
  • Measures that the owner of the call method is the same for both kinds
Contents

In the previous two lessons a block was only a piece of code accompanying a method call: it stayed where it was written and could not be stored directly in a variable. A proc turns the block into an object; a lambda is a stricter form of the same object. Both have the class Proc. A proc carries the variables of the scope where it was defined; M01/K02 called this property a closure, and it is not redefined here.

This lesson’s responder is the class: the Proc class answers the call message. The difference lies not in the class but in the object’s lambda? state.

Three Ways to Call

A lambda is written with ->. To call it you use call, the dot-parenthesis shorthand .(), or square brackets; the three are the same message. The core classifies this call: if the owner of call is a Class, the responder is a class.

# reply.rb — helpers that classify who answers a message

# Classification asks in order: is the expression a message at all (defined?),
# then do two different blocks give two different results, then who owns the method.
def responder(definition, owner: nil, result: nil, with_blocks: nil)
  return ["syntax", "defined? #{definition}"] unless definition == "method"

  if with_blocks
    first, second = with_blocks.call
    return ["block", "#{first.inspect} / #{second.inspect}"] unless first == second
  end

  kind = owner.instance_of?(Class) ? "class" : "module"
  [kind, "#{owner}, result #{result.inspect}"]
end
# lambda.rb — who answers the lambda call
require_relative "reply"

double = ->(n) { n * 2 }
owner = double.method(:call).owner
kind, evidence = responder(defined?(double.call(3)), owner: owner, result: double.call(3))
puts "#{kind}  #{evidence}"
puts double.class
puts double.call(3)
puts double.(3)
puts double[3]
class  Proc, result 6
Proc
6
6
6

The first line is the core’s lambda call observation: the responder is a class, and the owner is Proc. All three call forms gave 6. CB11. The three forms were measured with the same result in this lesson; which one is preferred is a matter of writing style and is not measured.

The Difference Between a Proc and a Lambda

The difference is not visible at first glance: both are called and both return a value. The distinction appears in argument checking. A lambda expects exactly the right number of arguments; a proc ignores the extra ones and fills in the missing ones with nil. In the error case, the class of the exception the lambda raises is printed, not its message.

# difference.rb — the argument difference between a proc and a lambda
loose = proc { |x| x }
strict = lambda { |x| x }
puts loose.call(1, 2)

result = begin
  strict.call(1, 2)
rescue ArgumentError => error
  error.class
end
puts result

pair = proc { |x, y| [x, y] }
puts pair.call(1).inspect
puts "loose=#{loose.lambda?} strict=#{strict.lambda?}"
1
ArgumentError
[1, nil]
loose=false strict=true

When given two arguments at once, the proc dropped the extra one and returned 1; the lambda raised ArgumentError on the same call. When the two-parameter proc was given a single argument, the missing parameter became nil. lambda? tells the two apart: false for the proc, true for the lambda.

Procs from Other Objects

In Ruby, a proc can be obtained from other sources too: to_proc from a method object and to_proc from a symbol. Both behave like a lambda.

# conversion.rb — a method and a symbol turn into a proc
def square(n)
  n * n
end

puts method(:square).to_proc.lambda?
puts :upcase.to_proc.lambda?
puts :upcase.to_proc.call("a")
true
true
A

The procs obtained from a method and from a symbol have a lambda? value of true. :upcase.to_proc.call("a") gave the uppercase string. This is the mechanism behind the select(&:even?) form seen in the previous lesson: & turns the symbol into a proc and passes it in place of the block.

Carrying a Variable

A proc stays bound to the variables of the scope where it was defined. To see this, a method returns a lambda, and the lambda keeps incrementing the variable even after the method has finished.

# counter.rb — a lambda carries the variable from where it was defined
def make_counter
  total = 0
  -> { total += 1 }
end

first = make_counter
second = make_counter
puts first.call
puts first.call
puts second.call
puts first.call
1
2
1
3

Each make_counter call set up its own total variable. first gave 1, 2 and 3 in turn; second gave 1 from its own counter. The method had finished, but the lambda kept carrying the variable. CB13. This measurement shows two independent counters; two lambdas sharing the same variable are not measured in this lesson.

Passing an Object in Place of a Block

A proc or a lambda can be passed to a method as a block by putting & in front of it. This joins map from the Blocks lesson with this lesson’s objects. arity, for its part, gives as a number how many parameters the object expects.

# as_block.rb — passing an object as a block, and arity
double = ->(n) { n * 2 }
loose = proc { |x, y| [x, y] }
puts [1, 2, 3].map(&double).inspect
puts "arity lambda=#{double.arity} proc=#{loose.arity}"
puts [[1, 2], [3, 4]].map(&loose).inspect
[2, 4, 6]
arity lambda=1 proc=2
[[1, 2], [3, 4]]

map(&double) handed each element to the lambda and collected [2, 4, 6]: there was no need to write a separate block. arity returned 1 for the lambda and 2 for the two-parameter proc. On the nested arrays, the proc split each inner array into two parameters and rebuilt the [x, y] array, so the result looked the same as the input. CB14. In this measurement the lambda was always called with a single element; what happens when a nested array is passed to a lambda with & was not measured.

Where return Returns To

Another difference between a proc and a lambda is where return goes. The two methods below do the same job: they build an object inside, call it, and then try to return 2.

# return_difference.rb — return inside a proc and a lambda
def with_proc
  pr = proc { return 1 }
  pr.call
  2
end

def with_lambda
  l = lambda { return 1 }
  l.call
  2
end

puts with_proc
puts with_lambda
1
2

with_proc returned 1: the return inside the proc ended not the proc but the method enclosing it, and the line with 2 was never reached. with_lambda returned 2: the return inside the lambda ended only the lambda, and the method carried on from where it was. CB30. The measurement was made only inside a method; a return inside a proc at the top level was not tried in this lesson.

Partial Application

curry turns a multi-parameter lambda into a chain that takes one parameter per call. When some of the parameters are given, a new object that waits for the rest is returned.

# curry.rb — calling parameter by parameter with curry
add = ->(a, b, c) { a + b + c }
puts add.curry[1][2][3]
puts add.curry.(1, 2).(3)
partial = add.curry[1]
puts partial.class
puts partial.lambda?
6
6
Proc
true

With three square-bracket calls and with the (1, 2).(3) form, curry gave the same result, 6. add.curry[1] returned a Proc, and this object was still a lambda. CB31. curry was measured only with a lambda of fixed parameters.

The Method Object

method(:name) turns a method into an object. This object can be called, its owner can be asked, and it can be turned into a block with &.

# method_object.rb — a Method object and to_proc with method
def square(n) = n * n
m = method(:square)
puts m.class
puts m.call(4)
puts "owner=#{m.owner} arity=#{m.arity}"
puts [1, 2, 3].map(&m).inspect
puts m.to_proc.class
Method
16
owner=Object arity=1
[1, 4, 9]
Proc

method(:square) gave a Method object and call(4) returned 16. The owner of square, defined at the top level, was printed as Object. map(&m) passed the method in place of a block and produced [1, 4, 9]; to_proc returned a Proc. CB32. The measurement was made only with a method defined at the top level.

Converting Between a Block and a Proc

If a method’s last parameter is &block, the block given to it is turned into a Proc object. Conversely, a lambda can be passed in place of a block with &. The measurement below looks at whether the object’s identity is preserved in these two directions.

# block_proc.rb — conversion between a block and a lambda
def take(&block) = block
b = take { |x| x + 1 }
puts b.class
puts b.lambda?
puts b.call(1)
l = ->(x) { x + 1 }
puts take(&l).equal?(l)
puts take(&l).lambda?
Proc
false
2
true
true

The object that came back from the block was a Proc, and lambda? gave false. When a lambda was passed with &l, take returned the same object: equal? was true and lambda? was still true. So & did not turn a lambda into a proc. CB33. The measurement was made only with a single-parameter block and one lambda.

Composing Callable Objects

>> and << combine two callable objects into a single Proc. f >> g applies f first and then g; f << g does the reverse. A Method object can take part in the same composition.

# compose.rb — composing procs
inc = ->(x) { x + 1 }
dbl = ->(x) { x * 2 }
puts (inc >> dbl).call(3)
puts (inc << dbl).call(3)
puts (inc >> dbl).class
puts (method(:Integer) >> inc).call("4")
8
7
Proc
5

(inc >> dbl).call(3) applied inc first and then dbl: (3 + 1) * 2 = 8. (inc << dbl).call(3) reversed the order: inc(dbl(3)) = 7. The result of the composition was a Proc. method(:Integer) >> inc composed a Method object with a lambda: "4" first became 4, then gave 5. CB45. The measurement was made with single-parameter lambdas.

arity and parameters

arity reports how many parameters a callable object expects, and parameters reports the kind of each. A proc and a lambda can report the same parameter list differently.

# arity.rb — arity and parameters
puts proc { |a, b| }.arity
puts lambda { |a, b| }.arity
puts proc { |*a| }.arity
puts lambda { |a, b = 1| }.arity
puts lambda { |a, b = 1| }.parameters.inspect
puts proc { |a, b| }.parameters.inspect
2
2
-1
-2
[[:req, :a], [:opt, :b]]
[[:opt, :a], [:opt, :b]]

proc { |a, b| } and lambda { |a, b| } both reported 2. *a printed -1, and one required plus one optional parameter printed -2: in both measurements the negative value was the negation of the required parameter count plus one. parameters shows the difference: in the lambda, a is required (:req), while in the proc the same parameter is optional (:opt). The looseness in the section The Difference Between a Proc and a Lambda can thus be read in the object’s own definition. CB46.

A Closure Shares the Variable

A lambda carries not the value of the outer local variable but the variable itself. If the variable changes later, the lambda sees the new value; if the lambda writes to the variable, the outer variable changes too.

# sharing.rb — a closure and the outer variable
x = 1
reader = -> { x }
x = 5
puts reader.call
writer = -> { x = 9 }
writer.call
puts x
5
9

When reader was defined, x was 1; x was later set to 5 and reader.call gave 5. The x = 9 inside writer wrote to the outer x, and puts x printed 9. A lambda uses the variable from where it was defined, not a copy. CB47. The measurement is for top-level locals; the case that crosses a method boundary was measured in the Carrying a Variable section.

A Lambda with case

A case expression tests a when condition with ===. Thanks to Proc#===, a lambda can take part in this test directly: === calls the lambda with the given value.

# case_lambda.rb — a lambda in a when branch
even = ->(n) { n.even? }
[1, 2].each do |n|
  case n
  when even then puts "#{n} even"
  else puts "#{n} odd"
  end
end
puts even === 2
puts "owner=#{even.method(:===).owner}"
1 odd
2 even
true
owner=Proc

when even called the lambda for every n: for 1 it returned false, so else ran; for 2 it returned true, so the when branch ran. even === 2 gave true directly, and the owner of === was printed as Proc. So when case tests the condition, it sends the === message to the lambda, and the responder is a class. This ties in with the common question of the Procs and Lambdas lesson: the same callable object can be used with call, with .(), in place of a block with &, and here with ===; in each, the object itself is the same and only the message changes. CB54. The measurement was made with a single-parameter lambda; the behavior of a multi-parameter lambda in a when branch was not tried. Since the value of the === branch is used in the case test itself, it was not assigned to a separate result variable in this measurement.

The Bounding Measurement

Can we see the difference between a proc and a lambda by looking at the owner of call? No: the owner is the same for both kinds, because both are instances of the Proc class. The difference is not in the owner but in the object’s state.

# owner.rb — the owner of the call message is the same for both kinds
loose = proc { |x| x }
strict = lambda { |x| x }
puts "proc    #{loose.method(:call).owner} lambda?=#{loose.lambda?}"
puts "lambda  #{strict.method(:call).owner} lambda?=#{strict.lambda?}"
proc    Proc lambda?=false
lambda  Proc lambda?=true

The owner is Proc on both lines; only lambda? differs. In the earlier lessons the owner question separated which class or module a method is defined in; here, because the two objects are of the same class, the same question does not reveal a difference. Sending the same message to two objects gives not two separate responders but two states of a single responder. So the owner question does not show the difference between a proc and a lambda: the class is the same, and the difference is carried by the object’s own state. CB12. This measurement asks only about the call message; whether there is an owner difference for other messages is not measured in this lesson.

Summary

  • A proc and a lambda are two ways of turning a block into an object; both have the class Proc.
  • This lesson’s responder is the class: the Proc answers the call message, and call, .() and square brackets give the same result.
  • A lambda checks the argument count and raises ArgumentError on an excess; a proc drops the extra and turns the missing into nil.
  • The procs obtained from a method object and from a symbol return true for lambda?.
  • The bounding measurement: the owner of call is Proc for both kinds; the difference is in the lambda? state.

Next Step

A lambda’s error was in the argument count. The next lesson moves on to the concept of an error itself: rescue is not a method but a construct of the language, and what this construct returns in which expressions will be measured.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close