--- /dev/null
+<?php
+
+namespace PFF;
+
+class Functions
+{
+ static function identity()
+ {
+ return new IdentityFunc;
+ }
+
+ static function func($func)
+ {
+ if ($func instanceof Func)
+ return $func;
+ else
+ return new Func($func);
+ }
+
+ static function bind()
+ {
+ $args = func_get_args();
+ $func = array_shift($args);
+ return new BoundFunc($func, $args);
+ }
+
+ static function curry($func, $arity)
+ {
+ return new BoundFunc($func, Placeholder::range($arity));
+ }
+
+ static function compose($f, $g)
+ {
+ return new \PFF\CompositionFunc($f, $g);
+ }
+
+ static function S($f, $g)
+ {
+ return new \PFF\SFunc($f, $g);
+ }
+}
+
+abstract class AbstractFunc
+{
+ abstract function apply($args);
+
+ function call()
+ {
+ return $this->apply(func_get_args());
+ }
+
+ function __invoke()
+ {
+ return $this->apply(func_get_args());
+ }
+
+ /* compose */
+
+ function then($then)
+ {
+ return new CompositionFunc($then, $this);
+ }
+
+ function thenBind()
+ {
+ $args = func_get_args();
+ $func = array_shift($args);
+ return $this->then(BoundFunc::createv($func, $args));
+ }
+}
+
+class Func extends AbstractFunc
+{
+ private $f;
+
+ function __construct($f)
+ {
+ $this->f = $f;
+ }
+
+ function apply($args)
+ {
+ return call_user_func_array($this->f, $args);
+ }
+}
+
+class IdentityFunc extends AbstractFunc
+{
+ function apply($args)
+ {
+ if (count($args) !== 1)
+ throw new \Exception('Wrong number of arguments. Only one is expected.');
+ return $args[0];
+ }
+}
+
+
+class Placeholder
+{
+ public $place;
+
+ function __construct($place)
+ {
+ $this->place = $place;
+ }
+
+ static function p($place=1)
+ {
+ return new self($place);
+ }
+
+ static function range($count)
+ {
+ $range = array();
+ for ($i = 1; $i <= $count; ++$i)
+ $range[] = self::p($i);
+ return $range;
+ }
+}
+
+class BoundFunc extends Func
+{
+ private $args, $arity;
+
+ function __construct($f, $args=null)
+ {
+ parent::__construct($f);
+ $this->args = $args;
+ $this->arity = 0;
+ foreach ($this->args as $p)
+ if ($p instanceof Placeholder)
+ $this->arity = max($this->arity, $p->place);
+ }
+
+ static function createv($func, $args)
+ {
+ return new BoundFunc($func, $args);
+ }
+
+ static function create()
+ {
+ $args = func_get_args();
+ $func = array_shift($args);
+ return self::createv($func, $args);
+ }
+
+ function apply($args)
+ {
+ if (count($args) < $this->arity) {
+ $px = Placeholder::range($this->arity - count($args));
+ return new self($this, array_merge($args, $px));
+ }
+
+ $a = array();
+ foreach ($this->args as $p) {
+ if ($p instanceof Placeholder) {
+ $a[] = $args[$p->place - 1];
+ } else {
+ $a[] = $p;
+ }
+ }
+ return parent::apply($a);
+ }
+}
+
+class CompositionFunc extends Func
+{
+ private $left;
+
+ function __construct($left, $right)
+ {
+ parent::__construct($right);
+ $this->left = Functions::func($left);
+ }
+
+ function apply($args)
+ {
+ return $this->left->call(parent::apply($args));
+ }
+}
+
+class SFunc extends Func
+{
+ private $left;
+
+ function __construct($left, $right)
+ {
+ parent::__construct($right);
+ $this->left = Functions::func($left);
+ }
+
+ function apply($args)
+ {
+ $r = parent::apply($args);
+ $args[] = $r;
+ return $this->left->apply($args);
+ }
+}
+
--- /dev/null
+<?php
+
+use \PFF\Functions as F;
+use \PFF\Placeholder as P;
+
+function second()
+{
+ return func_get_arg(1);
+}
+
+class FunctionTest extends PHPUnit_Framework_TestCase
+{
+ public function testBind()
+ {
+ $f = \PFF\BoundFunc::create('str_replace', ' ', '_', P::p());
+ $this->assertEquals('no_space', $f('no space'));
+
+ $f = \PFF\BoundFunc::create('sprintf', '[%s:%s:%s]', '[', P::p(), ']');
+ $this->assertEquals('[[:Abc:]]', $f('Abc'));
+ }
+
+ public function testCompose()
+ {
+ $f = F::func('abs')
+ ->then('sqrt');
+
+ $this->assertEquals(2, $f(4));
+ $this->assertEquals(2, $f(-4));
+ $this->assertEquals(9, $f(81));
+ }
+
+ public function testChain()
+ {
+ $before = ' text with_spaces underscores and-hypens ';
+ $after = '_text_with_spaces_underscores_and_hypens_';
+
+ $f = F::bind('str_replace', ' ', '_', P::p())
+ ->then(F::bind('str_replace', '-', '_', P::p()))
+ ->then(F::bind('str_replace', '__', '_', P::p()));
+
+ $this->assertEquals($after, $f($before));
+
+
+ $f = F::identity()
+ ->thenBind('str_replace', ' ', '_', P::p())
+ ->thenBind('str_replace', '-', '_', P::p())
+ ->thenBind('str_replace', '__', '_', P::p());
+
+ $this->assertEquals($after, $f($before));
+ }
+
+ public function testArrayBindDefault()
+ {
+ $f = F::bind(array('PFF\Arr', 'get'), P::p(1), P::p(2), 'DEFAULT');
+
+ $arr = array(
+ 'apple' => 100,
+ 'grapefruit' => 400,
+ 'carrot' => 50,
+ );
+
+ $this->assertEquals(400, $f($arr, 'grapefruit'));
+ $this->assertEquals('DEFAULT', $f($arr, 'orange'));
+ $this->assertEquals('DEFAULT', $f($arr, 'orange', 'no-oranges'));
+ }
+
+ public function testGetter()
+ {
+ $arr = array('name' => 'apple', 'weight' => 100);
+
+ $getter = F::bind(array('PFF\Arr', 'get'), P::p(2), P::p(1));
+ $getName = $getter('name');
+ $getWeight = $getter->call('weight');
+
+ $this->assertEquals('apple', $getName($arr));
+ $this->assertEquals(100, $getWeight($arr));
+ }
+
+ public function testPluck0()
+ {
+ $getName = F::bind(array('PFF\Arr', 'get'), P::p(), 'name');
+ $f = F::bind('array_map', $getName, P::p());
+
+ $arr = array(
+ array('name' => 'apple', 'weight' => 100),
+ array('name' => 'grapefruit', 'weight' => 400),
+ array('name' => 'carrot', 'weight' => 50),
+ );
+
+ $this->assertEquals(array('apple', 'grapefruit', 'carrot'), $f($arr));
+ }
+
+ public function testPluck1()
+ {
+ $f = F::compose(
+ F::curry('array_map', 2),
+ F::bind(array('PFF\Arr', 'get'), P::p(2), P::p(1)));
+
+ $arr = array(
+ array('name' => 'apple', 'weight' => 100),
+ array('name' => 'grapefruit', 'weight' => 400),
+ array('name' => 'carrot', 'weight' => 50),
+ );
+
+ $this->assertEquals(array('apple', 'grapefruit', 'carrot'), $f->call('name')->call($arr));
+ }
+
+ public function testPluck2()
+ {
+ $second = F::func('second');
+ $getter = F::bind(array('PFF\Arr', 'get'), P::p(2), P::p(1));
+ $pluck = F::S(
+ F::bind('array_map', P::p(3), P::p(1)),
+ F::compose($getter, $second));
+
+ $arr = array(
+ array('name' => 'apple', 'weight' => 100),
+ array('name' => 'grapefruit', 'weight' => 400),
+ array('name' => 'carrot', 'weight' => 50),
+ );
+
+ $this->assertEquals(array('apple', 'grapefruit', 'carrot'), $pluck($arr, 'name'));
+ }
+}
+