From e21a95d161d42a5065c0617544c18fa134c9d5da Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 10:00:19 -0500 Subject: [PATCH 01/11] Add curry() function Having an immutable `Curry` class allows creating a curry from any callable. Having this as a base class will allow additional functionality such as `Partial` to be added with minimal effort. --- README.md | 32 +++++++++++++++++++ composer.json | 13 +++++++- src/Curry.php | 77 ++++++++++++++++++++++++++++++++++++++++++++++ src/functions.php | 15 +++++++++ test/CurryTest.php | 61 ++++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/Curry.php create mode 100644 src/functions.php create mode 100644 test/CurryTest.php diff --git a/README.md b/README.md index 850b9f9..8ad8c31 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,38 @@ Combinators are simple functions that allow you to modify the behaviour of value These combinators aren't fully curried by default - mainly for optimisation reasons - but are designed so that most common use cases can be satisfied as is. Consequently, the type signatures use the comma (`,`) to represent multiple arguments. +### `curry :: a -> b -> c` + +Transform a callable that takes multiple parameters into a callable that takes one parameter and returns another function any more parameters are needed. + +```php +use function PhpFp\curry; + +$sum = curry(function ($a, $b, $c) { + return $a + $b + c; +}); + +// The following statements are all equivalent: +assert(6 === $add(1, 2, 3)); +assert(6 === $add(1)(2)(3)); +assert(6 === $add(1, 2)(3)); +assert(6 === $add(1)(2, 3)); +``` + +Note that currying ignores optional parameters. To curry optional parameters, set the arity: + +```php +use function PhpFp\curry; + +$concat = function ($a, $b, $join = '') { + return $a . $join . $b; +}; +$concat = curry($concat, 3); + +assert('a:b' === $concat('a', 'b', ':')); +``` + + ### `compose :: (b -> c), (a -> b) -> a -> c` Instead of writing `function ($x) { return f(g(x)); }`, `compose` allows us to express this as `compose('f', 'g')` (where the parameters could be closures, invokables, or anything that can be used as a "function"). Given two functions, `f` and `g`, a function will be returned that takes a value, `x`, and returns `f(g(x))`. This operation is _associative_, so `compose` calls can nest to create longer chains of functions. A simple, two-function example is shown here: diff --git a/composer.json b/composer.json index 1d57a3c..8e8a7f7 100644 --- a/composer.json +++ b/composer.json @@ -3,10 +3,21 @@ { "name": "Tom", "email": "tomjharding@live.co.uk" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "role": "Contributor" } ], "autoload": { - "files": ["src/Combinators.php"] + "psr-4": { + "PhpFp\\Combinators\\": "src/" + }, + "files": [ + "src/Combinators.php", + "src/functions.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Curry.php b/src/Curry.php new file mode 100644 index 0000000..e9366e5 --- /dev/null +++ b/src/Curry.php @@ -0,0 +1,77 @@ +fn = $fn; + $this->arity = $arity ?? $this->numberOfParameters($fn); + $this->args = []; + } + + /** + * Apply the curry with some parameters. + */ + public function __invoke(...$args) + { + $args = array_merge($this->args, $args); + + if (count($args) >= $this->arity) { + // All parameters have been defined, execute the function + return call_user_func_array($this->fn, $args); + } + + // Additional parameters are needed, return another curry + $copy = clone $this; + $copy->args = $args; + + return $copy; + } + + private function reflect(callable $fn): Reflector + { + if (is_array($fn)) { + return new ReflectionMethod($fn[0], $fn[1]); + } + + if (is_string($fn) && strpos($fn, '::')) { + return new ReflectionMethod($fn); + } + + if (is_object($fn)) { + return new ReflectionMethod($fn, '__invoke'); + } + + return new ReflectionFunction($fn); + } + + private function numberOfParameters(callable $fn): int + { + return $this->reflect($fn)->getNumberOfRequiredParameters(); + } +} diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..cd867fe --- /dev/null +++ b/src/functions.php @@ -0,0 +1,15 @@ + b -> c + */ +function curry(callable $fn, int $arity = null): callable +{ + return new Curry($fn, $arity); +} +define('curry', '\PhpFp\curry'); diff --git a/test/CurryTest.php b/test/CurryTest.php new file mode 100644 index 0000000..f759025 --- /dev/null +++ b/test/CurryTest.php @@ -0,0 +1,61 @@ +assertSame(6, $sum(1, 2, 3)); + $this->assertSame(6, $sum(1, 2)(3)); + $this->assertSame(6, $sum(1)(2, 3)); + $this->assertSame(6, $sum(1)(2)(3)); + + $concat = curry(static function ($a, $b, $join = '') { + return $a . $join . $b; + }, 3); + + $this->assertSame( + 'a:b', + $concat('a', 'b')(':'), + 'It curries exactly with optional arguments.' + ); + + $this->assertSame( + [1, 2], + curry('array_merge')([1], [2]), + 'It curries native functions.' + ); + + $this->assertSame( + 'Hello, world!', + curry([$this, 'say'])('world'), + 'It curries object methods.' + ); + + $this->assertSame( + 'Goodbye, test!', + curry(sprintf('%s::%s', self::class, 'bye'))('test'), + 'It curries static methods.' + ); + } + + public function say($name) + { + return "Hello, $name!"; + } + + public static function bye($name) + { + return "Goodbye, $name!"; + } +} From 7a0fcb3316244705333f9577d390a29ed67bad69 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 10:41:03 -0500 Subject: [PATCH 02/11] Switch to internal curry() function --- composer.json | 2 +- src/Combinators.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 8e8a7f7..4926b4e 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,6 @@ "phpunit/phpunit": "^5.7" }, "require": { - "cypresslab/php-curry": "^0.4.0" + "php": ">=7.0" } } diff --git a/src/Combinators.php b/src/Combinators.php index fc19341..fed7ab5 100644 --- a/src/Combinators.php +++ b/src/Combinators.php @@ -2,7 +2,7 @@ namespace PhpFp; -use Cypress\Curry as C; +use function PhpFp\curry; class Combinators { @@ -14,8 +14,8 @@ class Combinators */ public static function __callStatic($name, array $args) { - $f = C\curry([self::class, "{$name}_"]); - return count($args) ? $f(... $args) : $f; + $f = curry([self::class, "{$name}_"]); + return count($args) ? $f(...$args) : $f; } /** From 9d94745d1768dbaa27a6490d12250ece5d97bdb2 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 13:12:32 -0500 Subject: [PATCH 03/11] Add a changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b109df0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [2.0.0] + +### Added + +- `curry()` function + +### Removed + +- `Combinators` class has been removed in favor of function constructors + +## [1.0.0] + +- Initial release From 01b6e4793073aab822a695a1e0dd2ed44c901257 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 12:54:59 -0500 Subject: [PATCH 04/11] Add k() function --- CHANGELOG.md | 1 + README.md | 8 ++++---- src/Combinators.php | 10 ---------- src/functions.php | 11 +++++++++++ test/IfElseTest.php | 4 +++- test/KTest.php | 4 ++-- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b109df0..ca0c2aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - `curry()` function +- `k()` (Kestrel) function ### Removed diff --git a/README.md b/README.md index 8ad8c31..64cdf93 100644 --- a/README.md +++ b/README.md @@ -113,18 +113,18 @@ $collatz(3); // 10 $collatz(10); // 5 ``` -### `K :: a -> b -> a` +### `k :: a -> b -> a` This function takes a value, and then returns a function that always returns that value, regardless of what it receives. This creates a "constant" function to wrap a value in places where invocation is expected: ```php - b -> a + */ +function k($x): callable +{ + return static function () use ($x) { + return $x; + }; +} +define('k', '\PhpFp\k'); diff --git a/test/IfElseTest.php b/test/IfElseTest.php index fdd531d..deb0a83 100644 --- a/test/IfElseTest.php +++ b/test/IfElseTest.php @@ -4,6 +4,8 @@ use PhpFp\Combinators; +use function PhpFp\k; + class IfElseTest extends \PHPUnit_Framework_TestCase { public function testIfElse() @@ -12,7 +14,7 @@ public function testIfElse() $f = Combinators::ifElse( $isOdd, - Combinators::K('Oops!'), + k('Oops!'), function ($x) { return "$x is even!"; } diff --git a/test/KTest.php b/test/KTest.php index 21b2d39..91fb005 100644 --- a/test/KTest.php +++ b/test/KTest.php @@ -2,13 +2,13 @@ namespace PhpFp\Combinators\Test; -use PhpFp\Combinators; +use function PhpFp\k; class KTest extends \PHPUnit_Framework_TestCase { public function testK() { - $f = Combinators::K(3); + $f = k(3); $this->assertEquals( $f(2), From e0a5ff1c568aba5915ac6f412fc29ad37105e8bb Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 13:55:34 -0500 Subject: [PATCH 05/11] Add compose() function --- CHANGELOG.md | 1 + README.md | 11 +++++------ src/Combinators.php | 11 ----------- src/functions.php | 11 +++++++++++ test/ComposeTest.php | 4 ++-- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca0c2aa..8e0e8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- `compose()` function - `curry()` function - `k()` (Kestrel) function diff --git a/README.md b/README.md index 64cdf93..7e4a0a1 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,11 @@ assert('a:b' === $concat('a', 'b', ':')); Instead of writing `function ($x) { return f(g(x)); }`, `compose` allows us to express this as `compose('f', 'g')` (where the parameters could be closures, invokables, or anything that can be used as a "function"). Given two functions, `f` and `g`, a function will be returned that takes a value, `x`, and returns `f(g(x))`. This operation is _associative_, so `compose` calls can nest to create longer chains of functions. A simple, two-function example is shown here: ```php - c) -> (b, a) -> c` diff --git a/src/Combinators.php b/src/Combinators.php index f2545b0..dfd38f9 100644 --- a/src/Combinators.php +++ b/src/Combinators.php @@ -18,17 +18,6 @@ public static function __callStatic($name, array $args) return count($args) ? $f(...$args) : $f; } - /** - * Unary function composition. - * @param callable $f Outer function. - * @param callable $g Inner function. - * @return callable A composed unary function. - */ - public static function compose_(callable $f, callable $g, $value) - { - return $f($g($value)); - } - /** * Flip the arguments of a binary function. * @param callable $f The function to flip. diff --git a/src/functions.php b/src/functions.php index f10ba75..c796db1 100644 --- a/src/functions.php +++ b/src/functions.php @@ -5,6 +5,17 @@ use PhpFp\Combinators\Curry; +/** + * compose :: (b -> c), (a -> b) -> a -> c + */ +function compose(callable $f, callable $g): callable +{ + return static function ($x) use ($f, $g) { + return $f($g($x)); + }; +} +define('compose', '\PhpFp\compose'); + /** * curry :: a -> b -> c */ diff --git a/test/ComposeTest.php b/test/ComposeTest.php index 3990847..ec3fac0 100644 --- a/test/ComposeTest.php +++ b/test/ComposeTest.php @@ -2,7 +2,7 @@ namespace PhpFp\Combinators\Test; -use PhpFp\Combinators; +use function PhpFp\compose; class ComposeTest extends \PHPUnit_Framework_TestCase { @@ -18,7 +18,7 @@ public function testCompose() return $x / 2; }; - $f = Combinators::compose($halve, $inc); + $f = compose($halve, $inc); $this->assertEquals( $f(3), From aae02b2fe9b8b2f772078df238ab8f4f9c8b7c69 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 14:35:29 -0500 Subject: [PATCH 06/11] Add flip() function --- CHANGELOG.md | 1 + README.md | 15 ++++++--------- src/Combinators.php | 10 ---------- src/functions.php | 11 +++++++++++ test/FlipTest.php | 16 ++++++++++++++-- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0e8b3..647500e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `compose()` function - `curry()` function +- `flip()` function - `k()` (Kestrel) function ### Removed diff --git a/README.md b/README.md index 7e4a0a1..1761ce9 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,11 @@ assert('Hello, world' === $strictUcFirst('HELLO, WORLD')); Note that the functions are called **from left to right**. If the opposite is desired, this can be achieved easily: ```php - c) -> (b, a) -> c + */ +function flip(callable $f): callable +{ + return static function ($x, $y) use ($f) { + return $f($y, $x); + }; +} +define('flip', '\PhpFp\flip'); + /** * k :: a -> b -> a */ diff --git a/test/FlipTest.php b/test/FlipTest.php index 43dae88..08709ae 100644 --- a/test/FlipTest.php +++ b/test/FlipTest.php @@ -2,7 +2,9 @@ namespace PhpFp\Combinators\Test; -use PhpFp\Combinators; +use function PhpFp\compose; +use function PhpFp\curry; +use function PhpFp\flip; class FlipTest extends \PHPUnit_Framework_TestCase { @@ -13,12 +15,22 @@ public function testFlip() return $x - $y; }; - $subtractFrom = Combinators::flip($subtract); + $subtractFrom = flip($subtract); $this->assertEquals( $subtractFrom(2, 9), 7, 'Flips.' ); + + } + + public function testFlipCompose() + { + $ucstrict = compose('ucfirst', 'strtolower'); + $lcstrict = flip(curry(compose))('ucfirst', 'strtolower'); + + $this->assertSame('Abc', $ucstrict('ABC')); + $this->assertSame('abc', $lcstrict('ABC')); } } From b25527aae9674d0866daf69fdbcdf723cfdb4095 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 15:01:41 -0500 Subject: [PATCH 07/11] Add id() function --- CHANGELOG.md | 1 + README.md | 8 +++----- src/Combinators.php | 10 ---------- src/functions.php | 9 +++++++++ test/IdTest.php | 4 ++-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 647500e..8094948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `compose()` function - `curry()` function - `flip()` function +- `id()` function - `k()` (Kestrel) function ### Removed diff --git a/README.md b/README.md index 1761ce9..e6ee467 100644 --- a/README.md +++ b/README.md @@ -83,12 +83,10 @@ This function becomes much more useful for functions that are curried. It can be Returns whatever it was given! Again, this is useful in composition chains for the times when you don't want to do anything to the value (see `converge`): ```php - Bool), (a -> b), (a -> b) -> a -> b` diff --git a/src/Combinators.php b/src/Combinators.php index 2704249..c48095a 100644 --- a/src/Combinators.php +++ b/src/Combinators.php @@ -18,16 +18,6 @@ public static function __callStatic($name, array $args) return count($args) ? $f(...$args) : $f; } - /** - * Identity combinator. Return the parameter. - * @param mixed $x Anything in the world. - * @return mixed Exactly what $x was. - */ - public static function id_($x) - { - return $x; - } - /** * Conditional branching. * @param callable $p A boolean predicate. diff --git a/src/functions.php b/src/functions.php index c441879..527ccbe 100644 --- a/src/functions.php +++ b/src/functions.php @@ -36,6 +36,15 @@ function flip(callable $f): callable } define('flip', '\PhpFp\flip'); +/** + * id :: a -> a + */ +function id($x) +{ + return $x; +} +define('id', '\PhpFp\id'); + /** * k :: a -> b -> a */ diff --git a/test/IdTest.php b/test/IdTest.php index bde71eb..fbe01f8 100644 --- a/test/IdTest.php +++ b/test/IdTest.php @@ -2,7 +2,7 @@ namespace PhpFp\Combinators\Test; -use PhpFp\Combinators; +use function PhpFp\id; class IdTest extends \PHPUnit_Framework_TestCase { @@ -10,7 +10,7 @@ public function testId() { $this->assertEquals( 2, - Combinators::id(2), + id(2), 'Identity.' ); } From a8c7c2250d268e0c187ad7c1f42630f41086f0d3 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 15:26:06 -0500 Subject: [PATCH 08/11] Add if_else() function --- CHANGELOG.md | 1 + README.md | 20 +++++++++----------- src/Combinators.php | 12 ------------ src/functions.php | 11 +++++++++++ test/IfElseTest.php | 12 +++++++++--- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8094948..798166b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `curry()` function - `flip()` function - `id()` function +- `if_else()` function - `k()` (Kestrel) function ### Removed diff --git a/README.md b/README.md index e6ee467..af5c9e9 100644 --- a/README.md +++ b/README.md @@ -94,17 +94,16 @@ assert('hello' === id('hello')); This function allows for conditionals in composition chains. Unlike `converge`, which branches and merges, `ifElse` chooses which function to run based on the predicate, and the other function is ignored: ```php - b -> a` @@ -112,15 +111,14 @@ $collatz(10); // 5 This function takes a value, and then returns a function that always returns that value, regardless of what it receives. This creates a "constant" function to wrap a value in places where invocation is expected: ```php -use PhpFp\Combinators as F; - use function PhpFp\k; $isOdd = function ($x) { return $x % 2 === 1; }; -$f = F::ifElse($isOdd, k('Oops!'), function ($x) { return "$x is even!"; }); -$f(1); // 'Oops!' -$f(2); // '2 is even!' +$f = if_else($isOdd, k('Oops!'), function ($x) { return "$x is even!"; }); + +assert('Oops!' === $f(1)); +assert('2 is even!' === $f(2)); ``` ### `on :: (b, b -> c), (a -> b) -> a, a -> c` diff --git a/src/Combinators.php b/src/Combinators.php index c48095a..8b3c716 100644 --- a/src/Combinators.php +++ b/src/Combinators.php @@ -18,18 +18,6 @@ public static function __callStatic($name, array $args) return count($args) ? $f(...$args) : $f; } - /** - * Conditional branching. - * @param callable $p A boolean predicate. - * @param callable $f The "true" function. - * @param callable $g The "false" function. - * @return mixed The result of the chosen function. - */ - public static function ifElse_($p, $f, $g, $x) - { - return $p($x) ? $f($x) : $g($x); - } - /** * Psi combinator. * @param callable $f The outer function. diff --git a/src/functions.php b/src/functions.php index 527ccbe..e03a8a1 100644 --- a/src/functions.php +++ b/src/functions.php @@ -45,6 +45,17 @@ function id($x) } define('id', '\PhpFp\id'); +/** + * if_else :: (a -> Bool), (a -> b), (a -> b) -> a -> b + */ +function if_else(callable $p, callable $f, callable $g): callable +{ + return function ($x) use ($p, $f, $g) { + return $p($x) ? $f($x) : $g($x); + }; +} +define('if_else', '\PhpFp\if_else'); + /** * k :: a -> b -> a */ diff --git a/test/IfElseTest.php b/test/IfElseTest.php index deb0a83..905de72 100644 --- a/test/IfElseTest.php +++ b/test/IfElseTest.php @@ -2,8 +2,7 @@ namespace PhpFp\Combinators\Test; -use PhpFp\Combinators; - +use function PhpFp\if_else; use function PhpFp\k; class IfElseTest extends \PHPUnit_Framework_TestCase @@ -11,8 +10,15 @@ class IfElseTest extends \PHPUnit_Framework_TestCase public function testIfElse() { $isOdd = function ($x) { return $x % 2 === 1; }; + $odd = function ($x) { return $x * 3 + 1; }; + $even = function ($x) { return $x / 2; }; + + $collatz = if_else($isOdd, $odd, $even); + + $this->assertSame(10, $collatz(3)); + $this->assertSame(1, $collatz(2)); - $f = Combinators::ifElse( + $f = if_else( $isOdd, k('Oops!'), function ($x) { From b0d801993e6bd38d5290d67f7bc3154b4820e6b5 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 15:31:52 -0500 Subject: [PATCH 09/11] Add on() function --- CHANGELOG.md | 1 + README.md | 4 ++-- src/Combinators.php | 11 ----------- src/functions.php | 11 +++++++++++ test/OnTest.php | 4 ++-- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 798166b..25474b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `id()` function - `if_else()` function - `k()` (Kestrel) function +- `on()` function ### Removed diff --git a/README.md b/README.md index af5c9e9..edc9e4a 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ assert('2 is even!' === $f(2)); This function, also called the `Psi` combinator, allows you to call a function on transformations of values. This is really useful for things like sorting on particular properties of objects: we can call a compare function on two objects given a transformation. It's probably best illustrated with an example: ```php - 'something', 'test' => 4] ]; -array_column($sort($test, $f), 'title'); // ['goodbye', 'hello', 'something'] +assert(['goodbye', 'hello', 'something'] === $sort($test, $f), 'title'); ``` ## Contributing diff --git a/src/Combinators.php b/src/Combinators.php index 8b3c716..3ec7630 100644 --- a/src/Combinators.php +++ b/src/Combinators.php @@ -17,15 +17,4 @@ public static function __callStatic($name, array $args) $f = curry([self::class, "{$name}_"]); return count($args) ? $f(...$args) : $f; } - - /** - * Psi combinator. - * @param callable $f The outer function. - * @param callable $nt The parameter transformer. - * @return callable The transformed function. - */ - public static function on_(callable $f, callable $nt, $x, $y) - { - return $f($nt($x), $nt($y)); - } } diff --git a/src/functions.php b/src/functions.php index e03a8a1..2b30c0d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -66,3 +66,14 @@ function k($x): callable }; } define('k', '\PhpFp\k'); + +/** + * on :: (b, b -> c), (a -> b) -> a, a -> c + */ +function on(callable $f, callable $nt): callable +{ + return static function ($x, $y) use ($f, $nt) { + return $f($nt($x), $nt($y)); + }; +} +define('on', '\PhpFp\on'); diff --git a/test/OnTest.php b/test/OnTest.php index bf60931..76abdaa 100644 --- a/test/OnTest.php +++ b/test/OnTest.php @@ -2,7 +2,7 @@ namespace PhpFp\Combinators\Test; -use PhpFp\Combinators; +use function PhpFp\on; class OnTest extends \PHPUnit_Framework_TestCase { @@ -19,7 +19,7 @@ public function testOn() $sort = function ($xs, $f) use ($prop) { $ys = array_slice($xs, 0); - usort($ys, Combinators::on($f, $prop('test'))); + usort($ys, on($f, $prop('test'))); return $ys; }; From b70cbfe840a2349cbb11fd7294dd11b1e53dc951 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Wed, 31 May 2017 15:32:24 -0500 Subject: [PATCH 10/11] Remove combinators class --- composer.json | 1 - src/Combinators.php | 20 -------------------- 2 files changed, 21 deletions(-) delete mode 100644 src/Combinators.php diff --git a/composer.json b/composer.json index 4926b4e..9fbc830 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,6 @@ "PhpFp\\Combinators\\": "src/" }, "files": [ - "src/Combinators.php", "src/functions.php" ] }, diff --git a/src/Combinators.php b/src/Combinators.php deleted file mode 100644 index 3ec7630..0000000 --- a/src/Combinators.php +++ /dev/null @@ -1,20 +0,0 @@ - Date: Wed, 31 May 2017 23:49:06 -0500 Subject: [PATCH 11/11] Make combinators curried by default Having all combinators curried by default provides much greater flexibility for point-free programming. --- CHANGELOG.md | 12 ++++----- README.md | 4 +-- composer.json | 3 ++- src/curried.php | 62 +++++++++++++++++++++++++++++++++++++++++++++++ src/functions.php | 19 +-------------- test/FlipTest.php | 4 +-- 6 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 src/curried.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 25474b1..69ca62e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added -- `compose()` function -- `curry()` function -- `flip()` function -- `id()` function -- `if_else()` function -- `k()` (Kestrel) function -- `on()` function +- Add curried combinators as functions ### Removed - `Combinators` class has been removed in favor of function constructors +### Changed + +- Switched to a custom implementation of `curry()` + ## [1.0.0] - Initial release diff --git a/README.md b/README.md index edc9e4a..066b46a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Combinators are simple functions that allow you to modify the behaviour of value ## API -These combinators aren't fully curried by default - mainly for optimisation reasons - but are designed so that most common use cases can be satisfied as is. Consequently, the type signatures use the comma (`,`) to represent multiple arguments. +By default, all of the combinators (except `curry`) are curried by default. If you prefer to avoid currying for performance, all of the literal combinators are available in the `PhpFp\Combinators\` namespace. ### `curry :: a -> b -> c` @@ -52,7 +52,7 @@ $strictUcFirst = compose('ucfirst', 'strtolower'); assert('Hello, world' === $strictUcFirst('HELLO, WORLD')); ``` -Note that the functions are called **from left to right**. If the opposite is desired, this can be achieved easily: +Note that the functions are called **from left to right**. If the opposite is desired, this can be achieved easily by using `flip`: ```php use function PhpFp\compose; diff --git a/composer.json b/composer.json index 9fbc830..dadffba 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "PhpFp\\Combinators\\": "src/" }, "files": [ - "src/functions.php" + "src/functions.php", + "src/curried.php" ] }, "autoload-dev": { diff --git a/src/curried.php b/src/curried.php new file mode 100644 index 0000000..abcf31f --- /dev/null +++ b/src/curried.php @@ -0,0 +1,62 @@ + b -> c + */ +function curry(callable $fn, int $arity = null): callable +{ + return new Curry($fn, $arity); +} + +/** + * compose :: (b -> c), (a -> b) -> a -> c + */ +function compose(...$args) +{ + return curry('\PhpFp\Combinators\compose')(...$args); +} + +/** + * flip :: (a, b -> c) -> (b, a) -> c + */ +function flip(...$args) +{ + return curry('\PhpFp\Combinators\flip')(...$args); +} + +/** + * id :: a -> a + */ +function id(...$args) +{ + return curry('\PhpFp\Combinators\id')(...$args); +} + +/** + * if_else :: (a -> Bool), (a -> b), (a -> b) -> a -> b + */ +function if_else(...$args) +{ + return curry('\PhpFp\Combinators\if_else')(...$args); +} + +/** + * k :: a -> b -> a + */ +function k(...$args) +{ + return curry('\PhpFp\Combinators\k')(...$args); +} + +/** + * on :: (b, b -> c), (a -> b) -> a, a -> c + */ +function on(...$args) +{ + return curry('\PhpFp\Combinators\on')(...$args); +} diff --git a/src/functions.php b/src/functions.php index 2b30c0d..b521b03 100644 --- a/src/functions.php +++ b/src/functions.php @@ -1,9 +1,7 @@ c), (a -> b) -> a -> c @@ -14,16 +12,6 @@ function compose(callable $f, callable $g): callable return $f($g($x)); }; } -define('compose', '\PhpFp\compose'); - -/** - * curry :: a -> b -> c - */ -function curry(callable $fn, int $arity = null): callable -{ - return new Curry($fn, $arity); -} -define('curry', '\PhpFp\curry'); /** * flip :: (a, b -> c) -> (b, a) -> c @@ -34,7 +22,6 @@ function flip(callable $f): callable return $f($y, $x); }; } -define('flip', '\PhpFp\flip'); /** * id :: a -> a @@ -43,7 +30,6 @@ function id($x) { return $x; } -define('id', '\PhpFp\id'); /** * if_else :: (a -> Bool), (a -> b), (a -> b) -> a -> b @@ -54,7 +40,6 @@ function if_else(callable $p, callable $f, callable $g): callable return $p($x) ? $f($x) : $g($x); }; } -define('if_else', '\PhpFp\if_else'); /** * k :: a -> b -> a @@ -65,7 +50,6 @@ function k($x): callable return $x; }; } -define('k', '\PhpFp\k'); /** * on :: (b, b -> c), (a -> b) -> a, a -> c @@ -76,4 +60,3 @@ function on(callable $f, callable $nt): callable return $f($nt($x), $nt($y)); }; } -define('on', '\PhpFp\on'); diff --git a/test/FlipTest.php b/test/FlipTest.php index 08709ae..9eba7b4 100644 --- a/test/FlipTest.php +++ b/test/FlipTest.php @@ -3,7 +3,6 @@ namespace PhpFp\Combinators\Test; use function PhpFp\compose; -use function PhpFp\curry; use function PhpFp\flip; class FlipTest extends \PHPUnit_Framework_TestCase @@ -22,13 +21,12 @@ public function testFlip() 7, 'Flips.' ); - } public function testFlipCompose() { $ucstrict = compose('ucfirst', 'strtolower'); - $lcstrict = flip(curry(compose))('ucfirst', 'strtolower'); + $lcstrict = flip(compose())('ucfirst', 'strtolower'); $this->assertSame('Abc', $ucstrict('ABC')); $this->assertSame('abc', $lcstrict('ABC'));