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);
?>
Arguments to report_args: 1, 2, 3

You don't have to pass the arguments one at a time:

<?php
$x
= fp\curry('report_args');

$x(1, 2, 3);
?>
Arguments to report_args: 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);
?>
Arguments to report_args: 1, 2, 3

Indeed, you can specify all of the arguments:

<?php
$x
= fp\curry('report_args', 1, 2, 3);

$x();
?>
Arguments to report_args: 1, 2, 3

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()
  );
}
?>
Exception:
Class: Exception
Message: Too many arguments accumulated: 3 expected, 4 provided

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";
?>
getrandmax returns: 2147483647

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()
  );
}
?>
Exception:
Class: Exception
Message: argument 0 (counting from 0) must be an array

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()
  );
}
?>
Exception:
Class: Exception
Message: argument 0 (counting from 0) must be an instance of DateTime

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'
  
);
}
?>
A cube is a platonic solid.
A cuboctahedron is not a platonic solid.

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'
  
);
}
?>
A cube is a platonic solid.
A cuboctahedron is not a platonic solid.

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";
}
?>
The array is unchanged.

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";
  }
}
?>
Outcome:
Element 0: unchanged
Element 1: unchanged
Outcome:
Element 0: unchanged
Element 1: changed

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

  1. ^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.