From f8eb8a0cf7bcbb73536b623432beea1dfa13c93f Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 15 Jul 2018 22:27:03 +0200 Subject: [PATCH] add currying --- .../ReflectionPartiallyAppliedFunction.php | 6 + src/Utils/CurriedFunction.php | 72 ++++++++ src/Utils/PartiallyAppliedFunction.php | 2 +- src/Utils/index.php | 13 +- src/functions.php | 12 ++ tests/CurriedFunctionTest.php | 159 ++++++++++++++++++ .../ReflectionPartialAppliedFunctionTest.php | 12 ++ 7 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 src/Utils/CurriedFunction.php create mode 100644 tests/CurriedFunctionTest.php diff --git a/src/Reflection/ReflectionPartiallyAppliedFunction.php b/src/Reflection/ReflectionPartiallyAppliedFunction.php index 30321e7..c5b6162 100644 --- a/src/Reflection/ReflectionPartiallyAppliedFunction.php +++ b/src/Reflection/ReflectionPartiallyAppliedFunction.php @@ -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; + } } \ No newline at end of file diff --git a/src/Utils/CurriedFunction.php b/src/Utils/CurriedFunction.php new file mode 100644 index 0000000..5a13381 --- /dev/null +++ b/src/Utils/CurriedFunction.php @@ -0,0 +1,72 @@ + + * + * 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; + } +} \ No newline at end of file diff --git a/src/Utils/PartiallyAppliedFunction.php b/src/Utils/PartiallyAppliedFunction.php index 0a9c70c..65e6681 100644 --- a/src/Utils/PartiallyAppliedFunction.php +++ b/src/Utils/PartiallyAppliedFunction.php @@ -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) { diff --git a/src/Utils/index.php b/src/Utils/index.php index fde7190..bddbac6 100644 --- a/src/Utils/index.php +++ b/src/Utils/index.php @@ -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(); +} \ No newline at end of file diff --git a/src/functions.php b/src/functions.php index 0e3988f..267b28d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -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()); \ No newline at end of file diff --git a/tests/CurriedFunctionTest.php b/tests/CurriedFunctionTest.php new file mode 100644 index 0000000..05ee285 --- /dev/null +++ b/tests/CurriedFunctionTest.php @@ -0,0 +1,159 @@ + + * + * 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()); + } +} \ No newline at end of file diff --git a/tests/ReflectionPartialAppliedFunctionTest.php b/tests/ReflectionPartialAppliedFunctionTest.php index ea6027f..e643c0b 100644 --- a/tests/ReflectionPartialAppliedFunctionTest.php +++ b/tests/ReflectionPartialAppliedFunctionTest.php @@ -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()); + } } \ No newline at end of file