Currying with PHP 5.3

If you have used Caml (or some other functional programming languages) you may be familiar with currying functions. Unfortunately, currying isn't supported by PHP. Let's write a little hack to enable it.

Anonymous functions

First, let's have a quick look over anonymous functions in PHP 5.3. It's very simple to define one :

<?php
$func = function($x, $y) {
  return ($x + $y) * 2;
};
?>

Such functions are very useful to use as callbacks, especially when you need to give some "extra parameters" to your callback using the "use" keyword :

<?php
$z = 2;
$my_callback = function($x, $y) use ($z) {
  return ($x + $y) * $z;
};
?>

Do you know how PHP implements those anonymous functions ? It's quite simple, it's nothing but a class using the __invoke() magic method. See the Closure class documentation for more details.

Currying a function

You may have guess we'll use both anonymous functions and the __invoke() method to serve our purpose.

<?php
// Copyright (c) 2011 Rodolphe Breard
// 
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// 
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

class Curry
{
  protected $nb_params;
  protected $new;

  public function __construct($func)
  {
    if (!is_callable($func))
      throw new BadMethodCallException('Curry::__construct(): invalid callback');

    $args = func_get_args();
    unset($args[0]);
    $nb_args = sizeof($args);

    if (!($func instanceof Curry))
      {
        $r = new ReflectionFunction($func);
        $this->nb_params = $r->getNumberOfRequiredParameters() - $nb_args;
      }
    else
      $this->nb_params = $func->getNbParams() - $nb_args;

    $p_lst = array();
    for ($i = 0; $i < $this->nb_params; $i++)
      $p_lst[] = '$p' . $i;

    $a_lst = array();
    for ($i = 0; $i < $nb_args; $i++)
      $a_lst[] = '$args[' . ($i + 1) . ']';
    $a_lst = array_merge($a_lst, $p_lst);

    eval('$this->new = function(' . implode(',', $p_lst) . ') use($func, $args) { return $func(' . implode(',', $a_lst) . '); };');
  }

  public function __invoke()
  {
    return call_user_func_array($this->new, func_get_args());
  }

  public function getNbParams()
  {
    return $this->nb_params;
  }
}

?>

Wanna test this class ?

<?php

require 'Curry.php';


$func = function($x, $y) {
  return ($x + $y) * 2;
};

$b = new Curry($func, 3);
var_dump($b(0), $b(1), $b(5));
/*
 * int(6)
 * int(8)
 * int(16)
 */

$func = function($x, $y, $z) {
  return ($x + $y) * $z;
};

$b = new Curry($func, 3);
var_dump($b(0, 2), $b(1, 2), $b(5, 2));
/*
 * int(6)
 * int(8)
 * int(16)
 */

$b = new Curry($b, 2);
var_dump($b(0), $b(1), $b(5));
/*
 * int(0)
 * int(5)
 * int(25)
 */

$b = new Curry($func, 3, 2);
var_dump($b(0), $b(1), $b(5));
/*
 * int(0)
 * int(5)
 * int(25)
 */

$b = new Curry('strcmp');
var_dump($b('titi', 'toto'));
/*
 * int(-1)
 */

$b = new Curry('strcmp', 'titi');
var_dump($b('toto'));
/*
 * int(-1)
 */

$array = array('toto', 'titi', 'tutu');
$array_bis = array('toto', 'tutu');

$b = new Curry('in_array');
var_dump($b('titi', $array), $b('tata', $array));
/*
 * bool(true)
 * bool(false)
 */

$b = new Curry('in_array', 'titi');
var_dump($b($array), $b($array_bis));
/*
 * bool(true)
 * bool(false)
 */

?>

Tags