-
Notifications
You must be signed in to change notification settings - Fork 0
Switch to functional combinators #3
base: master
Are you sure you want to change the base?
Changes from all commits
e21a95d
7a0fcb3
9d94745
01b6e47
e0a5ff1
aae02b2
b25527a
a8c7c22
b0d8019
b70cbfe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# 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 | ||
|
||
- `compose()` function | ||
- `curry()` function | ||
- `flip()` function | ||
- `id()` function | ||
- `if_else()` function | ||
- `k()` (Kestrel) function | ||
- `on()` function | ||
|
||
### Removed | ||
|
||
- `Combinators` class has been removed in favor of function constructors | ||
|
||
## [1.0.0] | ||
|
||
- Initial release |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,44 +8,72 @@ 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: | ||
|
||
```php | ||
<?php | ||
use function PhpFp\compose; | ||
|
||
use PhpFp\Combinators as F; | ||
$strictUcFirst = compose('ucfirst', 'strtolower'); | ||
|
||
$strictUcFirst = F::compose('ucfirst', 'strtolower'); | ||
$strictUcFirst('HELLO, WORLD'); // Hello, world | ||
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 | ||
<?php | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for keeping these was that GitHub syntax highlighting didn't activate without them :( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
use function PhpFp\compose; | ||
use function PhpFp\flip; | ||
|
||
use PhpFp\Combinators as F; | ||
$pipe = flip(compose()); | ||
|
||
// The `flip` function is discussed later in this document. | ||
$pipe = flip(F::compose()); | ||
$pipe($f, $g)($x) === $g($f($x)); | ||
assert($pipe($f, $g)($x) === $g($f($x)); | ||
``` | ||
|
||
### `flip :: (a, b -> c) -> (b, a) -> c` | ||
|
||
This function takes a two-argument function, and returns the same function with the arguments swapped round: | ||
|
||
```php | ||
<?php | ||
|
||
use PhpFp\Combinators as F; | ||
use function PhpFp\flip; | ||
|
||
$divide = function ($x, $y) { return $x / $y; }; | ||
$divideBy = F::flip($divide); | ||
$divideBy = flip($divide); | ||
|
||
$divideBy(2, 10); // 5 | ||
assert(5 === $divideBy(2, 10)); | ||
``` | ||
|
||
This function becomes much more useful for functions that are curried. It can be useful in composition chains and point-free expressions. | ||
|
@@ -55,54 +83,50 @@ 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 | ||
<?php | ||
use function PhpFp\id; | ||
|
||
use PhpFp\Combinators as F; | ||
|
||
F::id(2); // 2 | ||
F::id('hello'); // hello | ||
assert(2 === id(2)); | ||
assert('hello' === id('hello')); | ||
``` | ||
|
||
### `ifElse :: (a -> Bool), (a -> b), (a -> b) -> a -> b` | ||
|
||
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 | ||
<?php | ||
|
||
use PhpFp\Combinators as F; | ||
use function PhpFp\if_else; | ||
|
||
$isOdd = function ($x) { return $x % 2 === 1; }; | ||
$odd = function ($x) { return $x * 3 + 1; }; | ||
$even = function ($x) { return $x / 2; }; | ||
|
||
$collatz = F::ifElse($isOdd, $odd, $even); | ||
$collatz(3); // 10 | ||
$collatz(10); // 5 | ||
$collatz = if_else($isOdd, $odd, $even); | ||
|
||
assert(10 === $collatz(3)); | ||
assert(5 === $collatz(2)); | ||
``` | ||
|
||
### `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 | ||
<?php | ||
|
||
use function PhpFp\Combinators; | ||
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` | ||
|
||
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 | ||
<?php | ||
use function PhpFp\on; | ||
|
||
// Get an array value. | ||
$prop = function ($k) | ||
|
@@ -131,7 +155,7 @@ $test = [ | |
['title' => 'something', 'test' => 4] | ||
]; | ||
|
||
array_column($sort($test, $f), 'title'); // ['goodbye', 'hello', 'something'] | ||
assert(['goodbye', 'hello', 'something'] === $sort($test, $f), 'title'); | ||
``` | ||
|
||
## Contributing | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
<?php | ||
declare(strict_types=1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately this does nothing. It only affects the type checking of functions you call in this file not their declarations.
You could however declare this on your test files where you actually make the calls and it would then have the desired affect. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does more than that, including forcing return type checks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But as per the note, it only makes a difference if set in the file calling the methods. E.g. // foo.php
<?php
declare(strict_types=1);
class A
{
public function test(int $a): int
{
return $a;
}
public function test2(int $a): int
{
return $this->test($a);
}
} // bar.php
<?php
require_once __DIR__ . '/foo.php';
$a = new A();
echo $a->test2('1'); // (int) 1 if you take the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah ignore me, that was a terrible test that missed the whole point because |
||
|
||
namespace PhpFp\Combinators; | ||
|
||
use ReflectionFunction; | ||
use ReflectionMethod; | ||
use Reflector; | ||
|
||
final class Curry | ||
{ | ||
/** | ||
* @var callable | ||
*/ | ||
private $fn; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
private $arity; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
private $args; | ||
|
||
/** | ||
* Wrap a callable in a curry. | ||
*/ | ||
public function __construct(callable $fn, $arity = null) | ||
{ | ||
$this->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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was mentioned elsewhere, but
$add
should be$sum