Partial Application & Currying in PHP
History
It’s been a while since I wrote about [currying] 1 in [Javascript] 2 and I’ve been meaning to do the same in PHP. I’ve had a prototype PHP implementation knocking around from the same time as my Javascript one, but I was never happy with it, specifically because it took a function’s parameter number explicitly. In other words, whereas in Javascript we can do this:
var my_func = curry(function(x, y) { return x + y; });
In PHP I would have to do this:
$my_func = $curry(2, function($x, $y) { return $x + $y; });
Note the explicit 2
, which tells
$curry
how
many parameters to accumulate before calling the function. I’ve since
revisited the code and removed this restriction, as well as implementing
a couple of alternatives.
Context
Code Smells, DRY and Expressions
Suppose we want to do a bunch of processing to a value. One way we might do it is imperatively, like this:
$val2 = $function1($val );
$val3 = $function2($val2);
$val4 = $function3($val3);
$val5 = $function4($val4);
$val6 = $function5($val5);
$val7 = $function6($val6);
$val8 = $function7($val7);
return $function8($val7);
This is clearly a mess, since there’s a very repetitive structure and we’ve got all kinds of intermediate results hanging around. It does the job, but this sort of redundancy and bloat can make larger, more complicated projects balloon in size and become difficult to understand.
There are a few ways we could improve this. For example, we might loop over the functions:
foreach ([$function1, $function2, $function3, $function4,
$function5, $function6, $function7, $function8] as $function) {
$val = $function($val);
}return $val;
That’s much cleaner, but now we’re clobbering the contents of $val
. We’re trying
to calculate a value, but we’re using statements to do
it. Statements are PHP’s way of sequencing side-effects (like variable
assignment), but we should be keeping side-effects to a minimum if we
want our code to be easy to understand and reusable. To calculate values
we should instead use expressions.
We can easily turn a loop statement into an expression by asking what
it is the loop is doing. In this case, we’re using the loop to turn a
collection of values (the array of functions) into a single value (the
result of sending $val
through
them). This is known as a fold or a reduction, so we
can use PHP’s [array_reduce
] 3 function:
$functions = [$function1, $function2, $function3, $function4,
$function5, $function6, $function7, $function8];
return array_reduce($functions,
function($result, $function) {
return $function($result);
,
}$val);
Here we’ve managed to get rid of most statements, but we’ve had to
write a whole new function (the second argument to array_reduce
) for
something which seems pretty basic: passing a value into a function. At
least if we pull out this function definition, we won’t have to redefine
such a simple thing ever again:
$pass_into = function($x, $f) { return $f($x); }
return array_reduce($functions, $pass_into, $val);
That array_reduce
now
looks nice and simple, and we’ve gained a reusable library function in
the process.
In fact PHP does have a function very similar to $pass_into
, known
as [call_user_func
] 4.
Unfortunately it takes its arguments the other way around:
$pass_into = function($x, $f) { return call_user_func($f, $x); }
return array_reduce($functions, $pass_into, $val);
Since we always want our code to be as generic as possible, we’re
actually better off abstracting $pass_into
into a
more general-purpose argument-flipping function, giving the final result
of:
$flip1 = function($f) {
return function($x, $y) use ($f) { return $f($y, $x); };
;
}
$functions = [$function1, $function2, $function3, $function4,
$function5, $function6, $function7, $function8];
return array_reduce($functions, $flip1('call_user_func'), $val);
Where $flip1
is a
general function which can be put into a library for reuse
elsewhere.
Flipping Awkwardness
Notice that our ‘flip’ function has a special structure: we accept
one argument $f
, then we return
a function which accepts another two arguments $x
and $y
, before finally
calling $f
with $y
and
$x
.
Why didn’t we use any of the following definitions instead?
// Accepts $f, $x and $y at the same time
$flip2 = function($f, $x, $y) { return $f($y, $x); };
// Accepts $f and $x then waits for $y
$flip3 = function($f, $x) {
return function($y) use ($f, $x) { return $f($y, $x); };
;
}
// Accepts $f and waits for $x, then waits for $y
$flip4 = function($f) {
return function($x) use ($f) {
return function($y) use ($f, $x) { return $f($y, $x); };
;
};
}
// etc.
It turns out that our definition of $flip1
coincides
exactly with the way array_reduce
handles its arguments,
but the other $flip
functions
don’t. However, there’s nothing inherently wrong with any of
those other definitions; they may be perfectly appropriate in different
circumstances.
So, should we keep all of them just in case? Well if we do, we will
need to consider even more cases: notice that in all of the
definitions above we are getting our result by calling $f($y, $x)
,
but what if $f
doesn’t take
both of its arguments at once? In particular, many of these $flip
functions
don’t take their arguments all at once, so we need even more
$flip
functions to handle all of these possible calling conventions:
$flip5 = function($f, $x, $y) {
return call_user_func($f($y), $x);
;
}
$flip6 = function($f) {
return function($x, $y) use ($f) {
return call_user_func($f($y), $x);
;
};
}
$flip7 = function($f) {
return function($x) use ($f) {
return function($y) use ($f, $x) {
return call_user_func($f($y), $x);
;
};
};
}
// etc.
Clearly we’re going too far down a rabbit hole here. How about we step back and consider a different solution, rather than writing more and more code to try and cover every possibility?
Partial Application
Real-World Example
We’ll start with a simple, but rather weak, solution: partial application. Recently at work I wrote a helper function to recursively insert a value at a given location in an array/object, something like the following:
// Usage: path value collection
$x = array_insert(['a', 'b', 'c'], 42, ['a' => new stdClass]);
// Result
$x['a']->b['c'] === 42
// In fact it's the inverse of an existing 'lookup' function, such that:
$path, $value, $collection), $path) === $value; lookup(array_insert(
An implementation of array_insert
is given below:
function array_insert($path, $val, $ao = array()) {
if (!$path) return $val;
$i = array_shift($path);
if (is_object($ao)) {
$ao->{$i} = array_insert($path,
$val,
isset($ao->{$i})? $ao->{$i} : array());
}else {
$ao [$i] = array_insert($path,
$val,
isset($ao [$i])? $ao [$i] : array());
}return $ao;
}
Clearly there is redundancy here, since the if/else branches
both call array_insert
with
$path
and
$val
. This
violates [DRY] 6 (Don’t
Repeat Yourself), but it’s difficult to see how we could remove the
redundancy since it’s a recursive call.
In fact, the way I solved this was to use partial application:
function array_insert($path, $val, $ao = array()) {
if (!$path) return $val;
$i = array_shift($path);
$f = papply('array_insert', $path, $val); // Partial application
if (is_object($ao)) $ao->{$i} = $f(isset($ao->{$i})? $ao->{$i} : array());
else $ao [$i] = $f(isset($ao [$i])? $ao [$i] : array());
return $ao;
}
So what’s going on here? Here’s the papply
function:
function papply() {
$args = func_get_args();
return function() use ($args) {
return call_user_func_array('call_user_func',
array_merge($args, func_get_args()));
;
} }
papply
will accept any number of
arguments, since it uses [func_get_args
] 7, then it returns a
function also accepting any number of arguments. These two argument
arrays are merged together and sent to call_user_func
.
Detailed Example
Here’s a trivial example of using papply
:
$replace_amps = papply('str_replace', '&', 'and');
The resulting closure, in this case called $replace_amps
, has
access to all of the arguments we gave, in this case 'str_replace'
,
'&'
and 'and'
.
Now we can pass some more arguments to this closure:
$my_string2 = $replace_amps($my_string1);
In this example, the merged arguments will be ['str_replace', '&', 'and', $my_string1]
.
The closure uses [call_user_func_array
]
8
to pass these as the arguments to another function (a process known as
[uncurrying] 9). It
just-so-happens to pass them all to call_user_func
,
which performs a regular function call. Let’s step through its
execution:
$my_string2 = $replace_amps($my_string1);
// Substitute in the definition of $replace_amps
$my_string2 = call_user_func(
'str_replace', '&', 'and'),
papply($my_string1);
// Substitute in the definition of papply
$my_string2 = call_user_func(
call_user_func(
function() {
$args = func_get_args();
return function() use ($args) {
return call_user_func_array(
'call_user_func',
array_merge($args, func_get_args()));
;
},
}'str_replace', '&', 'and'),
$my_string1);
// Apply the inner function
$my_string2 = call_user_func(
function() {
return call_user_func_array(
'call_user_func',
array_merge(['str_replace', '&', 'and'], func_get_args()));
,
}$my_string1);
// Apply the remaining function
$my_string2 = call_user_func_array(
'call_user_func',
array_merge(['str_replace', '&', 'and'], [$my_string1]));
// Apply array_merge
$my_string2 = call_user_func_array(
'call_user_func',
'str_replace', '&', 'and', $my_string1]);
[
// Apply call_user_func_array
$my_string2 = call_user_func(
'str_replace', '&', 'and', $my_string1);
// Apply call_user_func
$my_string2 = str_replace('&', 'and', $my_string1);
As you can see, the first argument we gave to papply ('str_replace'
)
will be called with the remaining arguments ('&'
,
'and'
)
and the arguments we gave to the resulting closure
($mystring1
).
If we step through the evaluation of array_insert
, we see the same thing
happening:
if (is_object($ao)) $ao->{$i} = $f(isset($ao->{$i})? $ao->{$i} : array());
else $ao [$i] = $f(isset($ao [$i])? $ao [$i] : array());
// Substitute in the definition of $f
if (is_object($ao)) $ao->{$i} = call_user_func(
'array_insert', $path, $val),
papply(isset($ao->{$i})? $ao->{$i} : array());
else $ao[$i] = call_user_func(
'array_insert', $path, $val),
papply(isset($ao[$i])? $ao[$i] : array());
// Substitute in the definition of papply
if (is_object($ao)) $ao->{$i} = call_user_func(
function() {
return call_user_func_array(
'call_user_func',
array_merge(['array_insert', $path, $val], func_get_args()));
;
},
}isset($ao->{$i})? $ao->{$i} : array());
else $ao[$i] = call_user_func(
function() {
return call_user_func_array(
'call_user_func',
array_merge(['array_insert', $path, $val], func_get_args()));
;
},
}isset($ao[$i])? $ao[$i] : array());
// Apply call_user_func
if (is_object($ao)) $ao->{$i} = call_user_func_array(
'call_user_func',
array_merge(['array_insert', $path, $val],
isset($ao->{$i})? $ao->{$i} : array()]));
[else $ao[$i] = call_user_func_array(
'call_user_func',
array_merge(['array_insert', $path, $val],
isset($ao[$i])? $ao[$i] : array()]));
[
// Apply array_merge
if (is_object($ao)) $ao->{$i} = call_user_func_array(
'call_user_func',
'array_insert', $path, $val,
[isset($ao->{$i})? $ao->{$i} : array()]);
else $ao[$i] = call_user_func_array(
'call_user_func',
'array_insert', $path, $val,
[isset($ao[$i])? $ao[$i] : array()]);
// Apply call_user_func_array
if (is_object($ao)) $ao->{$i} = call_user_func(
'array_insert', $path, $val,
isset($ao->{$i})? $ao->{$i} : array());
else $ao[$i] = call_user_func(
'array_insert', $path, $val,
isset($ao[$i])? $ao[$i] : array());
// Apply call_user_func
if (is_object($ao)) $ao->{$i} = array_insert(
$path, $val, isset($ao->{$i})? $ao->{$i} : array());
else $ao[$i] = array_insert(
$path, $val, isset($ao[$i])? $ao[$i] : array());
We’ve recovered the original, clunky code :)
Calling Conventions and Partial Application
In particular, notice that if we have a function which takes two
arguments, like $f = function ($x, $y) { ... }
,
then papply
lets us give each
argument in a separate step, e.g.
$g = papply($f, $x);
$g($y);
We can go even further by splitting the papply($f, $x)
call into two separate steps as well, using another call to
papply
:
$h = papply('papply', $f);
The resulting $h
function will
accept one set of arguments (e.g. $x
), then another
set of arguments (e.g. $y
), just as if it
were defined as function($x) { return function($y) use ($x) { ... }; }
.
Turning a function with one set of arguments into a function with two
sets of arguments is really useful, so we can make a generic function to
do this transformation. How? By using papply
again!
$splitArgs = papply('papply', 'papply');
With this general-purpose function, the above simplifies to:
$h = $splitArgs($f);
Simple Flipping
To see why this function is so useful, think back to our
proliferation of $flip
functions:
the reason we needed so many was because their calling conventions
differed in exactly the ways that $splitArgs
can
convert between. With papply
and
$splitArgs
in our tool box, we can massively simplify the $flip
functions:
// This is the only $flip function we need for $f($y, $x)
$flip = function($f, $x, $y) { return $f($y, $x); };
// We can implement the others via simple transformations
$flip1 = $splitArgs($flip);
$flip2 = $flip;
$flip3 = $flip1;
$flip4 = $splitArgs($flip1);
// This is the only $flip we need for call_user_func($f($y), $x)
$flip5 = function($f, $x, $y) {
return call_user_func($f($y), $x);
;
}
// The others are simple transformations
$flip6 = $splitArgs($flip5);
$flip7 = $splitArgs($flip6);
Advantages of Partial Application
One of the reasons we still need two forms of $flip
is because
these partially-applied functions are much weaker than proper curried
ones which I’ll explain further down. On the other hand, their advantage
is predictability: each call to papply
will delay a function call exactly
once, which means a single closure will be generated. This is important
in PHP since we don’t have [tail-call optimisation] 10, so we
always have to worry about overflowing the stack.
Another place where this can be especially useful is in our [array combinators] 11. For example, we can turn the following:
$a = array_map(function($x) { return str_replace('&', 'and', $x); },
$my_array1);
$b = array_filter($my_array2,
function($x) { return preg_match('/[0-9]+/', $x); });
Into:
$a = array_map(papply('str_replace', '&', 'and'), $my_array1);
$b = array_filter($my_array2, papply('preg_match', '/[0-9]+/'));
This is known as [eta-reduction] 12, and it turns up all over the place in PHP. For example, today I was reading the documentation for [Silex] 13. Here are some of their examples which can be easily eta-reduced:
// Original
function ($id) { return (int) $id; }
// Eta-reduced
'intval'
// Original
function () use ($app) {
return $app->redirect('/hello');
}
// Eta-reduced
&$app, 'redirect'], '/hello')
papply([
// Original
function () use ($app) {
// redirect to /hello
$subRequest = Request::create('/hello', 'GET');
return $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
// Eta-reduced
&$app, 'handle'], Request::create('/hello', 'GET'),
papply([SUB_REQUEST)
HttpKernelInterface::
// Original
function () use ($file) {
readfile($file);
}
// Eta-reduced
'readfile', $file) papply(
Proper Currying
Using papply
at work prompted me
to have another stab at a more general currying function, since I
realised I could use [reflection] 14
to overcome the parameter number issue. We can still use the $curry
function
shown near the beginning, with its explicit parameter number, but I’ve
renamed it to $curry_n
(ie.
‘curry N parameters’). The reflective replacement is simply:
$curry = $curry_n(1, function($f) use ($curry_n) {
return $curry_n(call_user_func([new ReflectionFunction($f),
'getNumberOfParameters']), $f);
; })
The $curry
function
will take a function $f
and pass it to
$curry_n
,
looking up the parameter number via reflection. Even though the $curry
function
only takes 1 parameter, we also curry it for reasons explained in the
Returning Functions section below.
As for the definition of $curry_n
, it’s
unfortunately a little complicated:
// Curries n-argument functions
$curry_n = call_user_func(function() {
// An uncurried version of $curry_n, encapsulated in this lambda
$_curry_n = function($n, $f) {
// An acceptor gathers $n arguments then calls $f
$make_acceptor = function($args) use ($n, $f, &$make_acceptor) {
return function() use ($n, $f, $args, &$make_acceptor) {
$args2 = array_merge($args, func_get_args());
if (count($args2) >= $n) {
// Send $n args to $f
$ret = call_user_func_array($f, array_slice($args2, 0, $n));
// Send any remaining args to the return value
return (count($args2) > $n)
? call_user_func_array($ret, array_slice($args2, $n))
: $ret;
}// Otherwise make an acceptor storing these arguments
return $make_acceptor($args2);
;
};
}// A curried function is just an acceptor without any args
return $make_acceptor([]);
;
}
// Only expose a curried version of $curry_n
return $_curry_n(2, $_curry_n);
; })
Motivation
So what does currying get us, compared to not currying? (I’ll ignore partial application until the Partial Application vs Currying section) Currying removes the arbitrary distinction PHP makes between functions with ‘different numbers of arguments’.
Like with the different $flip
functions
above, PHP will distinguish between functions like these three:
$foo1 = 'foo';
$foo2 = function($x, $y) {
return foo($x, $y);
;
}
$foo3 = function($x) {
return function($y) use ($x) {
return foo($x, $y);
;
}; }
These all call the foo
function,
but they differ in how they take their arguments:
$foo1
will accept any number of arguments and pass them all tofoo
$foo2
will accept any number of arguments and pass the first two tofoo
$foo3
will accept any number of arguments but keep only the first; it returns a function which likewise accepts any number of arguments and only keeps the first. The two arguments that were kept are passed tofoo
I discuss $foo1
in the
Taking All The Arguments section; for now let’s compare
$foo2
with
$foo3
.
The first thing to spot is that we can replace every occurrence of
$foo2($a, $b)
with call_user_func($foo3($a), $b)
,
yet we cannot replace occurrences of $foo3
using $foo2
. We need
some sort of wrapper, but that’s exactly what $foo3
already
is!
Hence out of these two options, we should always prefer to use $foo3
, since we
can easily transform it into $foo2
if
needed.
Unfortunately, functions like $foo3
cause a few
problems:
- The amount of boilerplate is greatly increased, since every
abstraction must be delimited with the
function
andreturn
keywords and we must explicitly list all of our free variables withuse
. - Applying several abstractions at the same time is painful, since
PHP’s lexer doesn’t accept chained function calls, ie.
$foo3(10)(20)
, which forces us to reify each application withcall_user_func
. - There are pretty much no PHP libraries/frameworks/etc. which do this, so we would be forced to switch back and forth between each convention.
These problems would all be solvable if PHP had some kind of
abstraction mechanism to encapsulate duplicate code in a reusable
component. Thankfully it does, they’re called functions! In fact the
$curry
and
$curry_n
functions defined above solve all of these problems!
$curry
The $curry
function
converts an uncurried function like $foo2
into a
curried form like $foo3
. This solves
half of problem (3), since it lets us curry existing libraries and
frameworks whenever we like.
Rather than accepting one argument at a time, like $foo3
, using $curry
and $curry_n
actually
lets us give any number of arguments at a time, including all
of them at once. In other words, we can still use PHP’s regular calling
convention $blah($x, $y, $z)
for curried functions. This solves problem (2) since we don’t need to
use call_user_func
to
supply extra arguments, and it also solves the remaining half of problem
(3) since existing libraries and frameworks will work perfectly well if
we give them curried functions.
What’s more, the $curry
and $curry_n
functions
allow us to write our own functions in an non-curried style
(avoiding problem (1)), which we can then pass through $curry
to get the
behaviour we want.
For example, let’s say we need some arithmetic functions. PHP doesn’t have any of the basic functions, so we need to write our own:
$curry_all = papply('array_map', $curry);
list($sum, $product, $subtract, $divide) = $curry_all([
function($x, $y) { return $x + $y; },
function($x, $y) { return $x * $y; },
function($x, $y) { return $x - $y; },
function($x, $y) { return $x / $y; }]);
These definitions are non-curried, like $foo2
, since they
take two arguments and don’t have any inner functions, but by passing
them through $curry
we’ve
curried them to behave like $foo3
,
accumulating arguments until they have two. For example:
// Apply an argument to each
$increment = $sum(1);
$negate = $subtract(0);
$double = $product(2);
$reciprocal = $divide(1);
// Some invariants
list($increment_test,
$negate_test,
$double_test,
$reciprocal_test) = $curry_all([
function($x) use ($increment) {
return $increment($x) === 1 + $x;
,
}function($x) use ($negate) {
return $negate($x) === 0 - $x;
,
}function($x) use ($double) {
return $double($x) === 2 * $x;
,
}function($x) use ($reciprocal) {
return $reciprocal($x) === 1 / $x;
;
}])
// We can still use the regular calling convention if we like
$sum (10, 20) === 30;
$product (3 , 4 ) === 12;
$subtract(10, 3 ) === 7;
$divide (10, 2 ) === 5;
What the $curry
function
does, as mentioned above, is pass its first argument to $curry_n
along
with its number of named parameters (ie. explicit arguments). It turns
out we don’t always want to use this number, since many PHP functions
use more (via func_get_args
) or
fewer (via default values). In these cases we can call $curry_n
directly.
$curry_n
The $curry_n
function
is the heart of the $curry
function
but can also be used standalone. It accepts a number $n
and a function
$f
which it
encapsulates in a closure, along with array $args
of arguments
which is initially empty. These closures are “acceptors” which only
exist to accept arguments.
If we ever call an acceptor, we will receive back another acceptor
encapsulating the same $n
and $f
, but its $args
array will
also have all of the arguments we passed in (if any) pushed on
to the end. Note that the original acceptor is not affected in any
way; the extra arguments are only stored in the new
acceptor we received back.
This new acceptor will behave exactly like the first, except for the
extra elements in its $args
array. If we
pass it some more arguments, we get back another new acceptor
which has those arguments pushed on to the end of its own $args
array, and
so on.
We can keep calling these acceptors as many times as we like, in any
order, passing in any amount of arguments we like. When any of the
acceptors manage to accumulate $n
arguments,
those arguments will be sent to the function $f
and the result
will be returned (instead of a new acceptor).
Whenever $f
is called,
nothing about any of the acceptors changes. We can carry on
calling them in any order with any number of arguments, just like
before.
A couple of reasons why we might want to use $curry_n
instead
of $curry
are:
- We want to use some of
$f
’s default parameters, but$curry
makes us provide a value for each one ourselves. In this case, we can use$curry_n
with the number of parameters we actually want to supply. $f
usesfunc_get_args
to look up a variable number of arguments, so it may not have any named parameters at all. Again, we can use$curry_n
to tell it how many parameters we want to supply.
If we didn’t use $curry_n
for these
cases, we would either end up calling our function without enough
parameters, or else we would get back another acceptor when we expect a
result.
Here’s an example of $curry_n
, where we
want to implode $n
strings:
// Some helper functions
// Takes an argument $n, then wraps the next $n arguments in an array.
// We use $curry_n on the inner function since it uses func_get_args.
$array = $curry(function($n) use ($curry_n) {
return $curry_n($n, function() { return func_get_args(); });
;
})
// Takes an $n and composes a function $f with an $n-ary function $g.
// We use $curry_n on the inner function since it uses func_get_args.
$compose_n = $curry(function($n, $f, $g) use ($curry_n) {
return $curry_n($n, function() use ($f, $g) {
return $f(call_user_func_array($g, func_get_args()));
;
});
})
// Takes $n arguments and implodes them with spaces
$space_maker = $curry(function($n) use ($compose_n, $curry, $array) {
return $compose_n($n, $curry('implode', ' '),
$array($n));
;
})
// Call $space_maker(10) in various equivalent ways
$sm_ = $space_maker(10);
$sm0 = $space_maker(10, 'zero');
$sm0123 = $sm0('one', 'two', 'three');
$sm_012345 = $sm_('zero', 'one', 'two', 'three', 'four', 'five');
$sm0123456 = $sm0123('four', 'five', 'six');
$sm_012345_ = $sm_012345();
$sm_0123456789 = $sm_('zero', 'one', 'two', 'three', 'four', 'five',
'six', 'seven', 'eight', 'nine');
$sm0123456789 = $sm0123456('seven', 'eight', 'nine');
$sm_012345_6789 = $sm_012345_('six', 'seven', 'eight', 'nine');
echo "$sm0123456789\\n";
echo "$sm_0123456789\\n";
echo "$sm_012345_6789\\n";
echo $space_maker(10, 'zero', 'one', 'two', 'three', 'four', 'five',
'six', 'seven', 'eight', 'nine');
This will print:
zero one two three four five six seven eight nine
zero one two three four five six seven eight nine
zero one two three four five six seven eight nine
zero one two three four five six seven eight nine
Since the $space_maker
function is curried, we’re free to call it however we like and as long
as it eventually gets enough arguments (10 in this case) it will produce
the imploded result.
Partial Application vs Currying
Currying is very similar to partial application shown above:
- Both will delay the evaluation of a function
- Both will work with regular, uncurried functions
- Both are idempotent
- If we don’t specify any extra arguments when we curry or partially-apply a function, the resulting closure will behave the same as the original function (although reflection will show them to differ).
However, currying is strictly more powerful:
- A partially applied function lets us specify some arguments now and the rest later, whereas a curried function will keep accepting batches of arguments again and again until it reaches its threshold $n.
- If we don’t specify any extra arguments when we curry a function, we can still pass it batches of arguments.
- The above makes it convenient to curry all functions as they’re defined.
- A curried function cannot be given an invalid number of arguments (although you may give it an incorrect number of arguments)
There is one more trick to currying compared to partial application, and that’s what happens when we give too many arguments.
Returning Functions
So far we’ve seen that $curry
and $curry_n
can
automatically turn a function like this:
$bar1 = function($x, $y) { return $x + $y; };
Into a function like this:
$bar2 = function($x) {
return function($y) use ($x) { return $x + $y; };
; }
However it converts the other way too! Our curried functions
are much more powerful than $bar1
or $bar2
, since they
let us use PHP’s regular calling convention $blah($x, $y)
on both.
Specifically if your curried function returns another function, it will all be treated as one. Here’s an example:
$output_if_both_positive = $curry(function($x, $y) {
return ($x > 0 && $y > 0)? function() {
var_dump(func_get_args());
return TRUE;
}: FALSE;
;
})
// Returns a closure
$output_if_both_positive(10);
// Returns FALSE
$output_if_both_positive(10, -4);
// Outputs ['hello', 'world'] and returns TRUE
$output_if_both_positive(10, 20, 'hello', 'world');
Note that this will work even if the returned function is not curried, since it will be passed all remaining arguments. However, it is especially useful when your returned functions are curried, because in that case they will only take as many arguments as they need and will pass the rest on to the next returned function.
This makes our life an awful lot simpler, since we no longer need to
care about using call_user_func
,
remembering which parameters need to go to which function, etc. We just
need to curry all of our functions when we define them, then throw all
of our arguments to the first function we call and it will send them all
to the right place. This is another reason why it’s important to set the
right number of parameters, eg. via $curry_n
; if we
don’t, our acceptors won’t know which arguments should be sent to their
encapsulated function $f
and which
should be sent to its return value.
Taking All The Arguments
There is one unanswered question: how do we implement $foo1
if all of
our functions are curried? Specifically, how do we accept all
subsequent arguments?
This sounds tricky, but it’s actually very simple. As far as
functions like $foo1
are
concerned, only the first batch of arguments matters, since $foo1
is not
curried so it can’t accept any more batches. Of course, foo
could be curried, but then there would
be no problem ;)
All we need to do is wrap our curried n-ary function in a non-curried function:
// If we're given a curried function like this
$foo4 = $curry('foo');
// We can wrap it in a non-curried function like this
$foo5 = function() use ($foo4) {
return call_user_func_array($foo4, func_get_args());
; }
$foo5
will accept all of the arguments it is given when called, then pass them
on to $foo4
which will either:
- Accept them all then wait for more, since it’s threshold hasn’t been reached
- Accept them all, call
foo
and return the result, if the number of arguments equals the threshold - Accept enough to call
$foo4
, then pass the rest to whatever return value it gets
What if we want to pass all of our (first batch of) arguments
directly to foo
, but we would like
to behave like a curried function thereafter? In that case we just need
to do the currying once we’ve got our arguments:
$foo6 = function() use ($curry_n) {
$args = func_get_args();
return call_user_func_array($curry_n(count($args), 'foo'),
$args);
; }
Drawbacks of Currying
Besides trying to further simplify the currying implementation itself, there are still some issues with this.
It’s nice that curried functions ‘take care of themselves’, but this makes their stack usage a bit more difficult to predict, especially if we’re currying all of our functions as we define them. As mentioned above, this is a problem since PHP doesn’t do tail-call optimisation. Some ways our stack usage may increase unnecessarily are:
- Calling a curried function without any arguments will give us back an equivalent function which uses one more stack frame.
- Curried functions which return curried functions which return curried functions, etc. can be called all at once, as the Returning Functions section demonstrates, but each subsequent call will be further down the stack.
- Currying a function multiple times, eg. to keep adjusting its argument number, will add to its stack usage.
Since PHP treats named functions differently to lambdas, it is more difficult than I’d like to curry a named function. In particular, we have to explicitly eta-expand the definitions of our named functions, in order to handle our arguments properly. For example, something like this:
function curry_n() {
$_curry_n = function($n, $f) {
$make_acceptor = function($args) use ($n, $f, &$make_acceptor) {
return function() use ($n, $f, $args, &$make_acceptor) {
$args2 = array_merge($args, func_get_args());
if (count($args2) >= $n) {
$ret = call_user_func_array($f, array_slice($args2, 0, $n));
return (count($args2) > $n)
? call_user_func_array($ret, array_slice($args2, $n))
: $ret;
}return $make_acceptor($args2);
;
};
}return $make_acceptor([]);
;
}
// Call $_curry_n(2, $_curry_n) with our arguments; ie. we're an
// eta-expansion of $_curry_n(2, $_curry_n)
return call_user_func_array(
$_curry_n(2, $_curry_n), func_get_args());
}
function curry() {
// Call curry_n(...) with our (remaining) arguments, ie. we're an
// eta-expansion of curry_n(...)
$args = func_get_args();
$f = array_shift($args);
return call_user_func_array(
call_user_func([new ReflectionFunction($f),
curry_n('getNumberOfParameters']), $f),
$args);
}
function flip() {
// Call curry(...) with our arguments, ie. we're an eta-expansion of
// curry(...)
return call_user_func_array(
function($f, $x, $y) { return $f($y, $x); }),
curry(func_get_args());
}
Of course, the idealist in me would avoid named functions altogether, since they’re global and globals should always be avoided (UPDATE: PHP now has namespaces, so the problem of globals is at least mitigated a little). However, the alternatives are:
- Using static class properties, which require indirection to call
(PHP assumes
Foo::bar()
is a method call), and classes are still global so we don’t gain much - Lexically scoping everything, but PHP’s
use
requirement gets old quickly and it would be nice if the ability to curry didn’t force a particular application architecture on us
For these reasons, I’ve not used this currying code in a production system yet.
On the other hand, I’ve found papply
to be extremely useful :)