Skip to content
This repository was archived by the owner on May 29, 2023. It is now read-only.

Curry all combinators #4

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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

- 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
96 changes: 60 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,74 @@ 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`

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:
Note that the functions are called **from left to right**. If the opposite is desired, this can be achieved easily by using `flip`:

```php
<?php
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.
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/functions.php",
"src/curried.php"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should both be exposed? The "uncurried" ones aren't all totally uncurried (thinking of compose, in particular), so it might be more helpful to hide the "original" functions and move them out of sight and mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They have to be imported somehow, or PHP won't find them. However, both are contained in namespaces so they won't pollute the global namespace.

]
},
"autoload-dev": {
"psr-4": {
Expand All @@ -27,6 +38,6 @@
"phpunit/phpunit": "^5.7"
},
"require": {
"cypresslab/php-curry": "^0.4.0"
"php": ">=7.0"
}
}
84 changes: 0 additions & 84 deletions src/Combinators.php

This file was deleted.

77 changes: 77 additions & 0 deletions src/Curry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);

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();
}
}
Loading