Currying in PHP
In this document we will see a PHP implementation of curry. We will see it first in its standard form, then in a generalised form.
A brief preliminary: we will assume that the following function has been defined.
<?php
function report_args($a, $b, $c,
$d = NULL, $e = NULL, $f = NULL)
{
printf(
"Arguments to report_args: %s\n",
implode(', ', func_get_args())
);
}
?>
And now to describe the software. To use it, download and extract this archive, then load the file functional.php into your application.
The software defines a number of things in the namespace kingfisher\functional, one of which is curry itself. Its first argument is what the PHP documentation refers to as a callback. Let's see it in use.
<?php
require_once 'functional.php';
use kingfisher\functional as fp;
# "fp" stands for
# "functional programming"
$x = fp\curry('report_args');
foreach (range(1, 3) as $n)
$x = $x($n);
?>
You don't have to pass the arguments one at a time:
<?php
$x = fp\curry('report_args');
$x(1, 2, 3);
?>
You can specify one or more of the arguments when you call curry:
<?php
$x = fp\curry('report_args', 1, 2);
$x(3);
?>
Indeed, you can specify all of the arguments:
<?php
$x = fp\curry('report_args', 1, 2, 3);
$x();
?>
But if you do this, then you can't specify any more arguments later on; doing so will cause an exception to be thrown:
<?php
$x = fp\curry('report_args', 1, 2, 3);
try {
$x(4);
} catch(Exception $e) {
printf(
"Exception:\n Class: %s\n Message: %s\n",
get_class($e),
$e->getMessage()
);
}
?>
How does curry decide how many arguments are needed before the underlying function is called? It uses the number of required arguments in the function's signature. For example, report_args has six arguments in total, but the last three are optional, so the number of required arguments is three.
Currying nullary functions is allowed:
<?php
$x = fp\curry('getrandmax');
echo "getrandmax returns: ", $x(), "\n";
?>
Type hinting is supported:
<?php
function f(array $a, $arg2) {}
$x = fp\curry('f');
try {
$x(1);
} catch(Exception $e) {
printf(
"Exception:\n Class: %s\n Message: %s\n",
get_class($e),
$e->getMessage()
);
}
?>
It works with objects too:
<?php
function f(DateTime $a, $arg2) {}
$x = fp\curry('f');
try {
$x(1);
} catch(Exception $e) {
printf(
"Exception:\n Class: %s\n Message: %s\n",
get_class($e),
$e->getMessage()
);
}
?>
curry generalised
Sometimes we would like to use currying, but can't. Consider the following script.
<?php
$is_platonic_solid =
function ($shape) {
return
in_array(
$shape,
array(
'tetrahedron', 'cube',
'octahedron', 'dodecahedron',
'icosahedron'
),
TRUE
);
};
$shapes = array('cube', 'cuboctahedron');
foreach ($shapes as $shape) {
printf(
"A %s %s a platonic solid.\n",
$shape,
$is_platonic_solid($shape)? 'is': 'is not'
);
}
?>
We can't curry in_array, because the arguments we want to specify are not grouped together at the start of the function signature.
This is where spatter1, a generalised form of curry, comes in. Let's see how it solves our problem.
<?php
$is_platonic_solid =
fp\spatter(
'in_array',
1, # skip 1 argument
array(
# this will be the second
# argument of in_array:
array(
'tetrahedron', 'cube',
'octahedron', 'dodecahedron',
'icosahedron'
),
# this will be the third
# argument of in_array:
TRUE
)
);
$shapes = array('cube', 'cuboctahedron');
foreach ($shapes as $shape) {
printf(
"A %s %s a platonic solid.\n",
$shape,
$is_platonic_solid($shape)? 'is': 'is not'
);
}
?>
spatter takes a variable number of arguments, with each argument being an array or an integer. (In fact, any non-array is assumed to be an integer and coerced to type integer using intval.)
An integer n means: "skip ahead n arguments — these arguments will be filled in later".
An array means: "use the contents of this array to fill in arguments, starting from the current position". These arguments will be passed to the underlying function if and when it is called.
You can now see what the above call to spatter does: it fills in the second and third arguments of in_array, leaving the first argument to be specified later.
Safety of spatter
As we saw earlier, curry arranges for the underlying function to be called as soon as all of its required arguments have been supplied. spatter behaves differently; you can think of it as performing the following computation.
<?php
function spatter() {
# ...
$n = 0;
foreach (func_get_args() as $x) {
$n +=
is_array($x)? count($x): intval($x);
}
/*
The underlying function will be
called as soon as $n arguments have
been supplied.
*/
# ...
}
?>
For example, suppose the underlying function has 6 arguments and you want to specify arguments 2 and 4. You will probably want to write something like this:
<?php
function senary($a, $b, $c, $d, $e, $f) {
# ...
}
$x = fp\spatter(
'senary',
1, # skip 1 argument
array($something),
1, # skip 1 argument
array($something_else),
2 # skip 2 arguments
);
foreach (range(1, 6) as $n)
$x = $x($n);
?>
If you omit the last argument to spatter (that is, the 2), then senary will be called when 4 arguments have been supplied; that is, on the fourth iteration of the foreach loop. The program will crash on the next iteration, if senary doesn't return something callable.
To eliminate the possibility of such an error, you can call safe_spatter instead. Its arguments have exactly the same interpretation as those of spatter, except that it checks them to ensure that you specified an appropriate number of arguments for the underlying function.
The number you specify must be no less than the number of required arguments, and no greater than the total number of arguments. For example, if the underlying function is report_args, then safe_spatter will throw an exception unless the number of arguments you specify is between 3 and 6.
You should use spatter only when safe_spatter is not an option.
Details
API
Everything defined by this software is in the namespace kingfisher\functional. The three functions curry, spatter, and safe_spatter consitute the API; anything else in the namespace should be considered private.
Reference arguments
If the underlying function has reference arguments, any changes that it makes to them will not be visible to your code. For example:
<?php
$nums = range(1, 9);
$a = $nums;
$x = fp\curry('rsort');
$x($a);
if($a === $nums) {
print "The array is unchanged.\n";
}
?>
However, if some argument is an array, and the array contains references, then changes to the argument may be visible. For an example, see the following script (and for an explanation, see the PHP documentation (the part beginning "references inside arrays are potentially dangerous")).
<?php
$a1 = array(range(1, 9), range(1, 9));
sort_arrays_descending(FALSE);
sort_arrays_descending(TRUE);
function sort_arrays_descending($make_ref) {
global $a1;
$a2 = $a1;
if($make_ref) {
$ref = &$a2[1];
}
$x = fp\curry('array_walk');
$x = $x($a2);
$x('rsort');
print "Outcome:\n";
foreach (array_keys($a2) as $k) {
echo "Element $k: ";
echo
$a2[$k] === $a1[$k]?
'unchanged': 'changed';
echo "\n";
}
}
?>
Blemishes
Visibility
Non-public methods can't be curried. There are times when this would be useful, but there seems to be no reasonable way to implement it without allowing non-public methods to be called where they shouldn't.
Specifying namespaces
If the specification of the underlying function contains the name of a function or the name of a class, and that name is not in the global namespace, then it must be fully qualified. The initial backslash may be omitted, however.
For example, suppose you are writing code in the namespace \IO\Unix, and you want to curry a function, in that same namespace, called open_file. Then you will have to write something like:
<?php
namespace IO\Unix;
# ...
function open_file() {
# ...
}
$x = fp\curry(__NAMESPACE__ . '\open_file');
?>
Similarly, if you want to curry the static method File::open, you will have to write something like this.
<?php
namespace IO\Unix;
# ...
class File {
static function open() {
# ...
}
}
$x = fp\curry(array(__NAMESPACE__ . '\File', 'open'));
?>
You can make your code more readable, and reduce the amount of typing you have to do, by defining a little function called ns.
<?php
namespace IO\Unix;
# ...
class File {
static function open() {
# ...
}
}
$x = fp\curry(array(ns('File'), 'open'));
function ns($name) {
return __NAMESPACE__ . '\\' . $name;
}
?>
Of course, this only helps if the name to be qualified is in the current namespace.
Licensing
The software (which doesn't have a name right now) is at version 0.9.1. It may be used as if it were in the public domain, and the same licence applies to this page.
Footnotes
- ^The name, which is rather fanciful, was chosen for lack of a better alternative. The intention is to capture the fact that the arguments to be pre-specified can be chosen arbitrarily.