Skip to content

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

  1. Write pipeline(*steps) that composes its arguments with reduce(:>>), and build a slug maker from :strip.to_proc, :downcase.to_proc and a lambda that turns runs of whitespace into -. Then curry a three-argument clamp(lo, hi, x) lambda into to_percent = clamp.curry[0][100] and map it over [-5, 42, 250].
  2. Write Proc#curry yourself: first curry2(f) returning ->(a) { ->(b) { f.(a, b) } }, then a version for any arity that reads f.arity and collects arguments until enough have arrived. Compare it with the real curry on add3, 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.

hello-big-world
["a-b", "c"]
[0, 42, 100]
to_percent.arity = -1, lambda? true
7
24

See also