Skip to content

Instantly share code, notes, and snippets.

@stanistan
Created October 1, 2012 02:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save stanistan/3809177 to your computer and use it in GitHub Desktop.
Save stanistan/3809177 to your computer and use it in GitHub Desktop.
Higher order functions in PHP: partial/curry/compose
<?php
/**
* Higher order functions in PHP
*
* Please let me know of any differences in the the uses and actual definitions of partial v. curry.
*/
/**
* Returns a partially applied function.
* Simplest usage:
* $map_trim = partial('array_map', 'trim');
* $map_trim([' a ', ' b']);
*
* @param callable $fn
* @param mixed ...
* @return callable
* @throws InvalidArgumentException
*/
function partial($fn)
{
if (!is_callable($fn)) {
throw new InvalidArgumentException('Given arg is not callable.');
}
$partial_args = array_slice(func_get_args(), 1);
return function() use($fn, $partial_args) {
return call_user_func_array($fn, array_merge($partial_args, func_get_args()));
};
}
/**
* Returns a curried function.
* If the function has indeterminate arguments, or optional arguments, the first arg
* to curry should be the number of arguments the function takes.
*
* For example, array_map potentially takes an infinite number of arguments,
* and using reflection we get: 2 required, 3 optional, but it can potentially be more.
*
* Example:
* $mapper = curry(2, 'array_map');
* $mapper = $mapper(); // will return a partially applied function (still awaiting args)
* $mapper = $mapper(); // will return a partially applied function (still awaiting args)
* $mapper = $mapper($some_func); // will return a partially applied function
* // with $some_func bound
* $mapper = $mapper(); // $some_func bound, still a partially applied function
* $mapped = $mapper($some_array); // evaluated map
*
* This is still pretty ugly in PHP, although if the feature to implement function
* dereferencing is added, the syntax will be nicer.
* @see https://wiki.php.net/rfc/fcallfcall
* Equivalent to above:
* $mapped = curry(2, 'array_map')()()($some_func)()($some_array);
*
* @param ... $num_args or $fn
* @param callable $fn
* @param ... mixed (args to bind)
* @return callable
* @throws InvalidArgumentException
*/
function curry()
{
$args = func_get_args();
$i = call_user_func(function() use($args) {
foreach ($args as $key => $a) {
if (is_callable($a)) {
return $key;
}
}
throw new InvalidArgumentException('No callable given to curry()');
});
$fn = $args[$i];
unset($args[$i]);
$args = array_values($args);
$get_fn_no_args = function($fn) {
$re = new ReflectionFunction($fn);
list($p, $r) = array(
$re->getNumberOfParameters(),
$re->getNumberOfRequiredParameters()
);
if ($p !== $r) {
throw new InvalidArgumentException(
'Specify the number of arguments expected in the first argument. '
. 'The given function has optional args.'
);
}
return $p;
};
$pars = ($i === 0) ? $get_fn_no_args($fn) : array_shift($args);
return function() use($fn, $pars, $args)
{
$args = array_merge($args, func_get_args());
return (count($args) >= $pars)
? call_user_func_array($fn, $args)
: call_user_func_array('curry', array_merge(array($pars, $fn), $args));
};
}
/**
* Returns a function which applies the given callables left to right
* Example:
* $print_trim = compose('trim', 'print_r');
* $print_trim('something');
*
* @param ... callable
* @return callable
* @throws InvalidArgumentException
*/
function compose()
{
$fns = func_get_args();
$cfns = array_filter($fns, 'is_callable');
if (!$fns || count($fns) !== count($cfns)) {
throw new InvalidArgumentException('
Invalid arguments to compose(). Excpeted callables.'
);
}
return function() use($fns) {
$args = func_get_args();
$re = call_user_func_array(array_shift($fns), $args);
foreach ($fns as $f) {
$re = $f($re);
}
return $re;
};
}
@slifin
Copy link

slifin commented Jul 8, 2015

when defining a curried function is there way of inserting a placeholder or reversing the order of parameters? I'm trying to define a parameter at the end of the function and leave an earlier parameter ready to accept data later

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment