add currying

This commit is contained in:
Kacper Donat 2018-07-15 22:27:03 +02:00
parent c9392f9b12
commit f8eb8a0cf7
7 changed files with 274 additions and 2 deletions

View File

@ -10,6 +10,7 @@
namespace Kadet\Functional\Reflection;
use Kadet\Functional\Utils\CurriedFunction;
use Kadet\Functional\Utils\PartiallyAppliedFunction;
class ReflectionPartiallyAppliedFunction extends \ReflectionFunction
@ -55,4 +56,9 @@ class ReflectionPartiallyAppliedFunction extends \ReflectionFunction
{
return array_diff_key(parent::getParameters(), $this->applied->getArguments());
}
public function isCurried()
{
return $this->applied instanceof CurriedFunction;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Copyright 2018 Kacper Donat
*
* @author Kacper "Kadet" Donat <kacper@kadet.net>
*
* Full license available in separate LICENSE file
*/
namespace Kadet\Functional\Utils;
use function Kadet\Functional\reflect;
class CurriedFunction extends PartiallyAppliedFunction
{
private $threshold;
/**
* CurriedFunction constructor.
*
* @param callable $callable
* @param int|null $threshold
* @param array $args
*/
public function __construct(callable $callable, int $threshold = null, ...$args)
{
parent::__construct($callable, ...$args);
$this->threshold = $threshold ?: reflect($callable)->getNumberOfRequiredParameters();
}
public function getThreshold(): int
{
return $this->threshold;
}
public function withThreshold(int $threshold = null)
{
return new static($this, $threshold);
}
public function apply(...$args)
{
return new static($this, $this->threshold, ...$args);
}
public function call(...$args)
{
return parent::__invoke(...$args);
}
public function uncurry()
{
return new PartiallyAppliedFunction($this);
}
/**
* @param mixed ...$args
*
* @return \Kadet\Functional\Utils\CurriedFunction|mixed
*/
public function __invoke(...$args)
{
$applied = $this->apply(...$args);
if (count($applied->getArguments()) >= $this->threshold) {
return $applied->call();
}
return $applied;
}
}

View File

@ -44,7 +44,7 @@ class PartiallyAppliedFunction implements Decorator
return ($this->callable)(...$arguments);
}
private function prepareArguments($args)
protected function prepareArguments($args)
{
$result = [];
foreach ($this->arguments as $argument) {

View File

@ -9,9 +9,20 @@
namespace Kadet\Functional;
use Kadet\Functional\Utils\CurriedFunction;
use Kadet\Functional\Utils\PartiallyAppliedFunction;
function partial(callable $callable, ...$args)
function partial(callable $callable, ...$args): PartiallyAppliedFunction
{
return new PartiallyAppliedFunction($callable, ...$args);
}
function curry(callable $callable, int $threshold = null, ...$args): CurriedFunction
{
return new CurriedFunction($callable, $threshold, ...$args);
}
function uncurry(CurriedFunction $curried)
{
return $curried->uncurry();
}

View File

@ -6,6 +6,8 @@ use Kadet\Functional\Predicate\AllOfPredicate;
use Kadet\Functional\Predicate\AnyOfPredicate;
use Kadet\Functional\Predicate\ClosurePredicate;
use Kadet\Functional\Predicate\ConstantPredicate;
use Kadet\Functional\Reflection\ReflectionPartiallyAppliedFunction;
use Kadet\Functional\Utils\PartiallyAppliedFunction;
require_once __DIR__.'/Utils/index.php';
@ -66,4 +68,14 @@ function symbol($name = null)
return sprintf("%s(%s)", $name ?: 'symbol', $id);
}
function reflect(callable $callable): \ReflectionFunctionAbstract
{
switch (true) {
case $callable instanceof PartiallyAppliedFunction:
return new ReflectionPartiallyAppliedFunction($callable);
default:
return new \ReflectionFunction($callable);
}
}
define(__NAMESPACE__.'\\_', symbol());

View File

@ -0,0 +1,159 @@
<?php
/**
* Copyright 2018 Kacper Donat
*
* @author Kacper "Kadet" Donat <kacper@kadet.net>
*
* Full license available in separate LICENSE file
*/
use Kadet\Functional as f;
use const Kadet\Functional\_;
class CurriedFunctionTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \Kadet\Functional\Utils\CurriedFunction
*/
private $function;
private $variadic;
private $optional;
protected function setUp()
{
$this->function = function ($a, $b, $c, $d) {
return $a . $b . $c . $d;
};
$this->variadic = function (...$args) {
return implode('', $args);
};
$this->optional = function($a, $b = 'b', $c = 'c') {
return $a . $b . $c;
};
}
public function testCallAfterAllArguments()
{
$f = f\curry($this->function);
$this->assertSame('abcd', $f('a')('b')('c')('d'));
$this->assertSame('abcd', $f('a')('b', 'c')('d'));
$this->assertSame('abcd', $f('a')('b', 'c', 'd'));
$this->assertSame('abcd', $f('a', 'b')('c', 'd'));
$this->assertSame('abcd', $f('a', 'b', 'c')('d'));
$this->assertSame('abcd', $f('a', 'b', 'c', 'd'));
}
public function testPartialApplication()
{
$f = f\curry($this->function);
$g = $f('a', 'b');
$this->assertSame(['a', 'b'], $g->getArguments());
}
public function testPartialApplicationWithPlaceholder()
{
$f = f\curry($this->function);
$g = $f('a', _, 'c');
$this->assertInstanceOf(f\Utils\PartiallyAppliedFunction::class, $g);
$this->assertSame([0 => 'a', 2 => 'c'], $g->getArguments());
$this->assertSame('abcd', $g('b', 'd'));
}
public function testOptionalArgumentsCall()
{
$f = f\curry($this->optional);
$this->assertSame('abc', $f('a'));
$this->assertSame('aBc', $f('a', 'B'));
$this->assertSame('aBC', $f('a', 'B', 'C'));
}
public function testVariadicThreshold()
{
$f = f\curry($this->variadic, 3);
$g = $f('a', _, 'c');
$this->assertSame('abc', $f('a', 'b', 'c'));
$this->assertInstanceOf(f\Utils\PartiallyAppliedFunction::class, $g);
$this->assertSame([0 => 'a', 2 => 'c'], $g->getArguments());
$this->assertSame('abc', $g('b'));
}
public function testForceApply()
{
$f = f\curry($this->variadic);
$g = $f->apply('a', 'b');
$this->assertSame('', $f());
$this->assertSame('abc', $f('a', 'b', 'c'));
$this->assertInstanceOf(f\Utils\PartiallyAppliedFunction::class, $g);
$this->assertSame(['a', 'b'], $g->getArguments());
$this->assertSame('abc', $g('c'));
}
public function testForceCall()
{
$f = f\curry($this->variadic, 10);
$g = $f->apply('a', 'b');
$this->assertSame('', $f->call());
$this->assertSame('abc', $f->call('a', 'b', 'c'));
$this->assertInstanceOf(f\Utils\PartiallyAppliedFunction::class, $g);
$this->assertSame(['a', 'b'], $g->getArguments());
$this->assertSame('abc', $g->call('c'));
}
public function testUncurry()
{
$f = f\curry($this->variadic, 10);
$f = $f('a', 'b');
$f = $f->uncurry();
$this->assertInstanceOf(f\Utils\PartiallyAppliedFunction::class, $f);
$this->assertSame(['a', 'b'], $f->getArguments());
$this->assertSame('abc', $f('c'));
}
public function testUncurryAlias()
{
$f = f\curry($this->variadic, 10);
$f = $f('a', 'b');
$f = f\uncurry($f);
$this->assertInstanceOf(f\Utils\PartiallyAppliedFunction::class, $f);
$this->assertSame(['a', 'b'], $f->getArguments());
$this->assertSame('abc', $f('c'));
}
public function testThresholdChange()
{
$f = f\curry($this->variadic, 10);
$g = $f->apply('a', 'b')->withThreshold(4);
$this->assertSame(4, $g->getThreshold());
$this->assertInstanceOf(f\Utils\PartiallyAppliedFunction::class, $g);
$this->assertInstanceOf(f\Utils\PartiallyAppliedFunction::class, $g('c'));
$this->assertSame('abcd', $g('c', 'd'));
}
public function testThreshold()
{
$f = f\curry($this->function);
$this->assertSame(4, $f->getThreshold());
$g = f\curry($this->optional);
$this->assertSame(1, $g->getThreshold());
$h = f\curry($this->variadic);
$this->assertSame(0, $h->getThreshold());
}
}

View File

@ -15,6 +15,9 @@ use Kadet\Functional\Reflection\ReflectionPartiallyAppliedFunction;
class ReflectionPartialAppliedFunctionTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \Kadet\Functional\Utils\PartiallyAppliedFunction
*/
private $function;
protected function setUp()
@ -67,4 +70,13 @@ class ReflectionPartialAppliedFunctionTest extends \PHPUnit\Framework\TestCase
return $parameter->getValue();
}, $parameters));
}
public function testIsCurried()
{
$reflection = new ReflectionPartiallyAppliedFunction($this->function);
$this->assertFalse($reflection->isCurried());
$curried = new ReflectionPartiallyAppliedFunction(f\curry($this->function->getDecorated()));
$this->assertTrue($curried->isCurried());
}
}