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¶
- Write
classify(name, callable)that printslambda?,arityand whatcall(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. - Write
first_even(array)that uses a proc'sreturninsideeachto leave the method with the first even number. Then store a lambda in a variable and call it insideeachinstead: the lambda'sreturnends only the lambda, so the method returns whatevereachreturned — the whole array.next,breakandreturnleave 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¶
- Blocks are not objects — the block that
&blockturns into a proc, never a lambda next,breakandreturnleave a block differently —returninside a block is the proc rule of row 6- Closures capture variables, not values — what both kinds carry with them
- Procs curry and compose —
arityandparametersagain, and whycurrycares about them - Arguments: positional, keyword and splat — the
ArgumentErrormessages a lambda shares with a method case/whenuses===— the lambda in awhenclause that row 9 prepares- Four kinds of equality —
===on aProcbeside===on a class, a range and a regexp - Method objects and unbound methods —
Method#to_proc, the lambda of row 11 - Rust: the three closure traits ↗ — Rust sorts closures by what they capture, not by how they treat arguments
- Rust: function pointers ↗ — a plain function as a value, the way
method(:puts).to_procis itand_1name the block's parameter — A block with no|x|can read its arguments as_1,_2, … (Ruby 2.7) or asit(Ruby 3.4); they are…&:symcallsSymbol#to_proc—&xin an argument list callsx.to_procand attaches the result as the block; a symbol'sto_procis a…- Assignment destructures, and blocks auto-splat —
a, b = 1, 2,a, *rest = listand(a, b), c = pair, 3read like Python - A DSL is
instance_evalplus stored blocks — A Ruby DSL is ordinary method calls made readable by three tricks - Twenty-three Python traps for a Ruby programmer — The twenty-three places where a Ruby reflex gives the wrong answer in Python