Procs curry and compose¶
Level: 201 · anyone who has used functools.partial and wants Ruby's equivalent — and the two things Python lacks
One line: Proc#curry turns an n-argument lambda into a chain of one-argument lambdas, >> and << compose any two callables into a new lambda, and parameters and arity describe what a proc or method expects; Python has functools.partial and inspect.signature, but no curry and no composition operator.
curry rewrites a lambda so that it can be fed its arguments one call at a time: add.curry[1][2] is 3, and so are .(1).(2) and [1, 2] given at once (row 1). Each partial step is a real lambda, so add.curry[1] can be kept as inc and used later (row 2), and a three-argument lambda accepts its arguments in any grouping (row 3). Currying a proc keeps the proc's rules — the result is still not a lambda — and a curried lambda reports arity -1, because each step accepts any number of arguments (row 4). A variadic lambda has no fixed count to wait for, so vsum.curry[1] simply calls it, and curry(3) tells it how many to collect; on a fixed-arity lambda the same curry(3) is an ArgumentError (row 5). Composition is two operators: f >> g is a lambda that runs f and then g, f << g runs g first (row 6). Both return a Proc that is a lambda, and both accept anything that answers call — a Method, a Proc, a Symbol#to_proc — so a chain of three composes and the result can be passed with & (row 7); a list of steps folds into a pipeline with reduce(:>>), and the difference between the two operators shows in sq << double against sq >> double (row 8). The right-hand side of >> may be any object with call, but the left-hand side must be a Proc or a Method, because >> is a method of those two classes (row 9). parameters and arity describe the shape of any of them — required, optional, rest, required keyword, keyword, keyword rest and block, with arity negative once the count is open — and Method#curry curries a method the same way (row 10).
Verified output of curry_and_composition_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. curry: one argument at a time, or all at once: add.curry[1][2] = 3, .(1).(2) = 3, [1, 2] = 3
2. a partly applied lambda is a value: inc = add.curry[1]; inc[41] = 42, Proc, lambda? true
3. three arguments, split any way: [1][2][3] = 6, [1, 2][3] = 6, [1][2, 3] = 6
4. curry on a proc keeps the proc rules: pr.curry[1][2] = [1, 2], lambda? false; a curried lambda's arity is -1
5. a variadic needs curry(n); a fixed one refuses: vsum.curry[1] = 1, vsum.curry(3)[1][2][3] = 6; add.curry(3): ArgumentError: wrong number of arguments (given 3, expected 2)
6. >> runs left then right, << the other way: (f >> g).call(1) = 20, (f << g).call(1) = 11
7. composition returns a lambda, from any callables: Proc, lambda? true; (double >> :to_s.to_proc >> :size.to_proc).call(50000) = 6, map(&(double >> f)) [3, 5]
8. a pipeline by reduce(:>>); order matters: [double, sq, f].reduce(:>>).call(3) = 37, (sq << double).call(3) = 36, (sq >> double).call(3) = 18
9. anything with call composes on the right: (double >> callable).call(1) = 102; callable >> double: NoMethodError, a plain object has no >>
10. parameters and arity describe the shape: add [[:req, :a], [:req, :b]] 2, pr [[:opt, :a], [:opt, :b]] 2, full [[:req, :a], [:opt, :b], [:rest, :c], [:keyreq, :d], [:key, :e], [:keyrest, :f], [:block, :g]] -3, method(:f).curry[4] = 5
Reading the output¶
Rows 1 to 3 print 3 and 6 from every way of splitting the arguments, and row 2 shows the intermediate step is a Proc with lambda? true. Row 4 is [1, 2] from a curried proc with lambda? false, then -1 for the arity of a curried lambda. Row 5 prints 1 for vsum.curry[1] — a variadic lambda is called at the first argument — then 6 for vsum.curry(3) fed three, then the ArgumentError message wrong number of arguments (given 3, expected 2) for add.curry(3). Row 6 is 20 for f >> g, which added 1 and then multiplied, and 11 for f << g, which multiplied and then added. Row 7 names the class of a composition, Proc, with lambda? true, then 6 — 50000 doubled is 100000, six characters — and [3, 5] from a composition passed to map with &. Row 8 is 37 from double, sq, f in that order on 3, then 36 and 18 from the two directions. Row 9 is 102 from a callable object on the right and NoMethodError from the same object on the left. Row 10 prints the parameter lists: two required for the lambda, two optional for the proc, and all seven kinds for the full signature with an arity of -3 — one required positional plus one required keyword, and the list is open — then 5 from a curried Method.
Compared with Python¶
Python's functools.partial is the nearest thing to curry, and it is a different thing: it applies some arguments now and returns a callable that takes the rest in one call, so partial(add, 1)(2) is 3 but nothing chains by itself — currying by hand is a lambda returning a lambda (row 1). A partial remembers its func, args and keywords (row 2), nests to split three arguments any way (row 3), can fix a keyword argument, and checks nothing early: inspect.signature(inc) shows the one parameter left (row 4), too many arguments fail only when the partial is called, and a variadic accepts any split (row 5). There is no composition operator — f >> g on two functions is a TypeError — and the idiom is a two-line compose returning a lambda (row 6); it returns a function, takes any callables including str and len (row 7), folds into a pipeline with functools.reduce, and shows the same order sensitivity (row 8). Because compose is a plain function rather than a method of Proc, an object with __call__ can stand on either side (row 9). inspect.signature is parameters and arity in one object: the printed signature, the parameter count, and each parameter's kind (row 10). The pieces are documented under functools.partial ↗, functools.reduce ↗ and inspect.signature ↗.
Verified output of curry_and_composition_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. partial: some arguments now, the rest later: partial(add, 1)(2) = 3, by hand: curried(1)(2) = 3, partial(add, 1, 2)() = 3
2. a partial is a value that remembers its pieces: inc = partial(add, 1); inc(41) = 42, partial, func add, args (1,), keywords {}
3. three arguments, split any way: partial(partial(add3, 1), 2)(3) = 6, partial(add3, 1, 2)(3) = 6, partial(add3, 1)(2, 3) = 6
4. a partial by keyword; nothing checks arity early: partial(add, b=2)(1) = 3, keywords {'b': 2}; signature(inc) (b)
5. a variadic takes any split; too many fails at the call: partial(vsum, 1)(2, 3) = 6, partial(vsum, 1, 2)(3) = 6; partial(add, 1, 2, 3)(): TypeError
6. no >> on functions; compose is two lines: f >> g: TypeError; compose(f, g)(1) = 20, compose(g, f)(1) = 11
7. compose returns a function, from any callables: function; compose(compose(double, str), len)(50000) = 6, map(compose(mul2, f)) [3, 5]
8. a pipeline by reduce(compose); order matters: reduce(compose, [double, sq, f])(3) = 37, compose(double, sq)(3) = 36, compose(sq, double)(3) = 18
9. anything with __call__ composes, on either side: compose(double, Callable())(1) = 102, compose(Callable(), double)(1) = 202
10. inspect.signature describes the shape: add (a, b) 2, full (a, b=1, *c, d, e=2, **f), kinds ['POSITIONAL_OR_KEYWORD', 'POSITIONAL_OR_KEYWORD', 'VAR_POSITIONAL', 'KEYWORD_ONLY', 'KEYWORD_ONLY', 'VAR_KEYWORD']
Try it¶
- Write
pipeline(*steps)that composes its arguments withreduce(:>>), and build a slug maker from:strip.to_proc,:downcase.to_procand a lambda that turns runs of whitespace into-. Then curry a three-argumentclamp(lo, hi, x)lambda intoto_percent = clamp.curry[0][100]and map it over[-5, 42, 250]. - Write
Proc#curryyourself: firstcurry2(f)returning->(a) { ->(b) { f.(a, b) } }, then a version for any arity that readsf.arityand collects arguments until enough have arrived. Compare it with the realcurryonadd3, including the[1, 2][3]grouping of row 3.
Solution to 1
curry_and_composition_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: pipeline(*callables) composes with reduce(:>>); a slug maker
# is built from three steps, and a three-argument clamp is curried down to
# a one-argument percentage clamp.
def pipeline(*steps) = steps.reduce(:>>)
slug = pipeline(:strip.to_proc, :downcase.to_proc, ->(s) { s.gsub(/\s+/, "-") })
puts slug.call(" Hello Big World ")
puts [" A B ", "c"].map(&slug).inspect
clamp = ->(lo, hi, x) { x.clamp(lo, hi) }
to_percent = clamp.curry[0][100]
puts [-5, 42, 250].map(&to_percent).inspect
puts "to_percent.arity = #{to_percent.arity}, lambda? #{to_percent.lambda?}"
# the same clamp, fully applied in one go, and then composed after slug's length
puts clamp.curry[0, 100, 7]
puts (slug >> :size.to_proc >> to_percent).call(" a very long title indeed ")
Verified output of curry_and_composition_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
&:symcallsSymbol#to_proc— the procs that row 7 chains with>>- Procs and lambdas differ in arity and
return—arityandparametersfor the two kinds, before currying - Closures capture variables, not values — each curried step is a closure over the arguments so far
- Method objects and unbound methods —
Method#curry,Method#>>andto_proc - Arguments: positional, keyword and splat — the seven parameter kinds row 10 lists
tap,thenand chaining — a pipeline written withtheninstead of>>- Rust: what a closure is ↗ — a closure returning a closure, which is what
currybuilds - Rust: adapters by job ↗ — composition as a chain of adapters rather than an operator