Skip to content

Procs and lambdas differ in arity and return

Level: 201 · anyone who has typed both proc and lambda and wondered why Ruby keeps two

One line: Every proc and lambda is a Proc, but a lambda checks its argument count and returns from itself, while a proc pads and drops arguments and returns from the method that created it.

Ruby has four spellings for a callable object — Proc.new { }, proc { }, lambda { } and -> { } — and all four produce a Proc; the only difference is the lambda? flag, false for the first two and true for the last two (row 1). That flag changes two behaviours. Arguments: a proc treats its parameters like block parameters, so a missing one is nil, an extra one is dropped, and a single array argument is spread across |a, b| (row 2); a lambda treats them like method parameters, so each of those three calls is an ArgumentError (rows 3 and 4). return: inside a lambda it ends the lambda and the calling method carries on (row 5); inside a proc it ends the method the proc was written in (row 6), and if that method has already returned there is nothing left to return from, which is a LocalJumpError (row 7). arity counts mandatory parameters and reports -n-1 when the list is open-ended, and the two kinds disagree on |a, b = 1|: a proc says 1, because its optional parameters are finite and ignored, a lambda says -2 (row 8). Any Proc can be called four ways — call, .(), [], and yield once it is passed with & — and === calls it too, which is what lets a lambda stand in a when clause (row 9). The flag is fixed at creation: lambda(&a_proc) does not convert a proc — in Ruby 4.0 it is an ArgumentError (row 10) — and the procs Ruby builds for you from a Symbol or a Method are lambdas (row 11).

Verified output of procs_and_lambdas_differ_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. Proc.new, proc, lambda, -> are all Proc:     Proc, lambda? false false true true
 2. a proc pads, drops, and splats one array:    call(1) = [1, nil], call(1, 2, 3) = [1, 2], call([1, 2]) = [1, 2]
 3. a lambda checks the count, no splat:         ArgumentError (given 1), ArgumentError (given 3), ArgumentError (given 1)
 4. the lambda's message:                        wrong number of arguments (given 1, expected 2)
 5. return in a lambda:                          lambda returned 10, method continues
 6. return in a proc:                            method returned 10
 7. return in a proc whose method is gone:       LocalJumpError: unexpected return
 8. arity, proc then lambda:                     |a, b| 2 2, |a, b = 1| 1 -2, |*a| -1, none 0
 9. four ways to call, and === for case/when:    call 9, .() 9, [] 9, yield 9; big === 7: true
10. lambda(&a_proc) turns it into a lambda?      ArgumentError: the lambda method requires a literal block
11. Symbol#to_proc, Method#to_proc are lambdas:  true true

Reading the output

Row 1 prints one class name for four constructors and the lambda? flags false false true true. Row 2 is the lenient kind: call(1) gives [1, nil], call(1, 2, 3) keeps [1, 2], and call([1, 2]) spreads the one array over both parameters. Row 3 puts the same three calls to a lambda and every one is an ArgumentError; row 4 prints the message of the first, wrong number of arguments (given 1, expected 2), which is the message a method would give. Rows 5 to 7 are return: the lambda's return 10 came back as the value of l.call and the method printed its own string; the proc's return 10 became the method's value, so "never reached" was not; and the orphaned proc — created by make_orphan and called after make_orphan had returned — raised LocalJumpError: unexpected return. Row 8 lists arity for a proc and a lambda side by side: 2 and 2 for |a, b|, then 1 against -2 for |a, b = 1|, -1 for |*a|, 0 for no parameters. Row 9 gets 9 from all four call spellings and true from big === 7. Row 10 shows Ruby 4.0 refusing lambda(&pr) with the lambda method requires a literal block, and row 11 that :upcase.to_proc and method(:puts).to_proc both answer lambda? with true.

Compared with Python

Python has one function object. A lambda and a def are both a function (row 1); both refuse a wrong argument count with a TypeError — one argument, three, or a single tuple where two parameters were declared (rows 2 and 3; the message is not printed because CPython rewords it between releases, row 4) — and there is no lenient kind, since spreading a tuple is explicit with *. return only ever leaves the function it is written in: a lambda's value and a nested def's return both hand control back to the outer function (rows 5 and 6), and a function called after its outer function has finished simply returns its value (row 7) — Ruby's non-local return and its LocalJumpError have no counterpart. Where Ruby has arity, Python has inspect.signature, which prints the whole parameter list (row 8). A function is called with (), through __call__, or with operator.call (row 9); there is no ===, because match decides by isinstance and == rather than by calling a predicate. What a Python lambda cannot do is hold a statement: lambda: x = 1 is a SyntaxError, and anything beyond one expression needs a def (row 10). Row 11 is the counterpart of Ruby's row 11: str.upper and the bound 'a'.upper are callables of their own types, method_descriptor and builtin_function_or_method. The pieces are documented under lambda expressions ↗, inspect.signature ↗ and operator.call ↗.

Verified output of procs_and_lambdas_differ_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. lambda and def are both function:            function function, no proc/lambda split
 2. a lambda given 1, 3, or one tuple:           TypeError (given 1), TypeError (given 3), TypeError (given 1)
 3. a def given 1, 3, or one tuple: the same:    TypeError (given 1), TypeError (given 3), TypeError (given 1)
 4. the message is not printed:                  CPython rewords TypeError messages between releases
 5. return in a lambda:                          inner returned 10, outer continues
 6. return in a nested def:                      still reached: return leaves inner only
 7. return in a def whose outer is gone:         returns 1 - no error, a function never returns from another
 8. inspect.signature, lambda then def:          (a, b) (a, b=1) (*a) () (a, b)
 9. three ways to call, no case-equality:        sq(3) 9, sq.__call__(3) 9, operator.call(sq, 3) 9
10. a statement inside a lambda:                 SyntaxError - a lambda is one expression, def takes the rest
11. str.upper and 'a'.upper are callables too:   True True (method_descriptor, builtin_function_or_method)

Try it

  1. Write classify(name, callable) that prints lambda?, arity and what call(1) returns or raises, and run it over six callables written with two parameters — |a, b|, |a, *b| and |a, b = 9|, each as a proc and as a lambda. Predict first which of the six raise.
  2. Write first_even(array) that uses a proc's return inside each to leave the method with the first even number. Then store a lambda in a variable and call it inside each instead: the lambda's return ends only the lambda, so the method returns whatever each returned — the whole array. next, break and return leave a block differently has the fix, break.
Solution to 1

procs_and_lambdas_differ_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: classify(callable) prints lambda?, arity, and what call(1)
# does when the callable was written with two parameters.

def classify(name, callable)
  result = begin
    callable.call(1).inspect
  rescue ArgumentError => e
    "#{e.class}: #{e.message}"
  end
  puts "#{name.ljust(20)} lambda? #{callable.lambda?.to_s.ljust(5)} arity #{callable.arity.to_s.rjust(2)}  call(1) -> #{result}"
end

classify "proc { |a, b| }", proc { |a, b| [a, b] }
classify "->(a, b) { }", ->(a, b) { [a, b] }
classify "proc { |a, *b| }", proc { |a, *b| [a, b] }
classify "->(a, *b) { }", ->(a, *b) { [a, b] }
classify "proc { |a, b = 9| }", proc { |a, b = 9| [a, b] }
classify "->(a, b = 9) { }", ->(a, b = 9) { [a, b] }

Verified output of procs_and_lambdas_differ_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

proc { |a, b| }      lambda? false arity  2  call(1) -> [1, nil]
->(a, b) { }         lambda? true  arity  2  call(1) -> ArgumentError: wrong number of arguments (given 1, expected 2)
proc { |a, *b| }     lambda? false arity -2  call(1) -> [1, []]
->(a, *b) { }        lambda? true  arity -2  call(1) -> [1, []]
proc { |a, b = 9| }  lambda? false arity  1  call(1) -> [1, 9]
->(a, b = 9) { }     lambda? true  arity -2  call(1) -> [1, 9]

See also