loophp/combinator

A curated list of combinators

Fund package maintenance!
drupol

Installs: 1 430

Dependents: 1

Suggesters: 0

Security: 0

Stars: 121

Watchers: 5

Forks: 2

Open Issues: 7

pkg:composer/loophp/combinator

2.0.2 2021-08-04 20:13 UTC

README

Latest Stable Version GitHub stars Total Downloads GitHub Workflow Status Scrutinizer code quality Type Coverage Code Coverage License Donate!

Combinator

This package provides a comprehensive collection of well-known combinators for PHP, enabling a more declarative, point-free programming style.

Description

A combinator is a higher-order function that uses only function application and previously defined combinators to define a result from its arguments. This concept was introduced by Moses Schönfinkel in 1920 and later developed by Haskell Curry.

In computer science, combinatory logic serves as a theoretical model of computation and is a cornerstone of functional programming language design. The core idea is to build complex functions without ever having to mention variables.

Why use Combinators?

Using combinators can help you:

  • Write cleaner, more declarative code: By abstracting away function composition and argument manipulation, your code can become more readable and expressive.
  • Avoid temporary variables: Combinators provide powerful ways to pipe data through a series of functions in a clean, fluent manner.
  • Explore functional programming: This library provides a practical way to learn about and experiment with fundamental concepts of functional programming and lambda calculus, right within PHP.

Requirements

  • PHP >= 8.0

Installation

You can install the package via Composer:

composer require loophp/combinator

Available Combinators

The following is a list of the combinators available in this package.

Name Alias Haskell Lambda Calculus Term Definition (JS-like) Type
A Apply $ λab.ab a => b => a(b) (a -> b) -> a -> b
B Bluebird . λabc.a(bc) a => b => c => a(b(c)) (b -> c) -> (a -> b) -> a -> c
Blackbird ... λabcd.a(bcd) a => b => c => d => a(b(c)(d)) (c -> d) -> (a -> b -> c) -> a -> b -> d
C Cardinal flip λabc.acb a => b => c => a(c)(b) (a -> b -> c) -> b -> a -> c
D Dove λabcd.ab(cd) a => b => c => d => a(b)(c(d)) (b -> c -> d) -> a -> (b -> c) -> a -> d
E Eagle λabcde.ab(cde) a => b => c => d => e => a(b)(c(d)(e)) (c -> d -> e) -> a -> (b -> c -> d) -> b -> c -> e
F Finch λabc.cba a => b => c => c(b)(a) a -> b -> (b -> a -> c) -> c
G Goldfinch λabcd.ad(bc) a => b => c => d => a(d)(b(c)) (c -> b) -> (a -> d -> c) -> a -> d -> b
H Hummingbird λabc.abcb a => b => c => a(b)(c)(b) (a -> b -> a -> c) -> a -> b -> c
I Idiot id λa.a a => a a -> a
J Jay λabcd.ab(adc) a => b => c => d => a(b)(a(d)(c)) (a -> c -> d) -> a -> b -> (a -> c) -> d
K Kestrel const λab.a a => b => a a -> b -> a
Ki Kite konst λab.b a => b => b a -> b -> b
L Lark λab.a(bb) a => b => a(b(b)) (a -> a -> b) -> (a -> b)
M Mockingbird λa.aa a => a(a) (a -> a) -> a
O Owl λab.b(ab) a => b => b(a(b)) ((a -> b) -> a) -> (a -> b) -> b
Omega Ω λa.(aa)(aa) a => (a(a))(a(a)) (a -> a) -> b
Phoenix λabcd.a(bd)(cd) a => b => c => d => a(b(d))(c(d)) (b -> c -> d) -> (a -> b) -> (a -> c) -> a -> d
Psi on λabcd.a(bc)(bd) a => b => c => d => a(b(c))(b(d)) (b -> b -> c) -> (a -> b) -> a -> a -> c
Q Queer (##) λabc.b(ac) a => b => c => b(a(c)) (a -> b) -> (b -> c) -> a -> c
R Robin λabc.bca a => b => c => b(c)(a) a -> (b -> a -> c) -> b -> c
S Starling <*> λabc.ac(bc) a => b => c => a(c)(b(c)) (a -> b -> c) -> (a -> b) -> a -> c
S' S Prime <*> λabc.a(bc)c a => b => c => a(b(c))(c) (b -> a -> c) -> (a -> b) -> a -> c
S₂ S-Two <*> λabcd.a((bd)(cd)) a => b => c => d => a(b(d)(c(d))) (c -> d) -> (a -> b -> c) -> (a -> b) -> a -> d
T Thrush (&) λab.ba a => b => b(a) a -> (a -> b) -> b
U Turing λab.b(aab) a => b => b(a(a)(b)) ((a -> b) -> b) -> (a -> b) -> b
V Vireo λabc.cab a => b => c => c(a)(b) a -> b -> (a -> b -> c) -> c
W Warbler λab.abb a => b => a(b)(b) (a -> a -> b) -> a -> b
Y Y-Fixed point fix λf.(λx.f(xx))(λx.f(xx)) f => (x => f(x(x)))(x => f(x(x))) (a -> a) -> a
Z Z-Fixed point fix λf.(λx.f(λv.xxv))(λx.f(λv.xxv)) f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v))) (a -> a) -> a

Combinator by Example

Click on any combinator below to see a practical usage example. All combinators are invokable classes, but the easiest way to access them is through the loophp\combinator\Combinators facade, which statically provides each one.

A (Apply) Combinator
  • Lambda: λab.ab
  • Purpose: Applies a function a to an argument b. In Haskell, this is the $ operator. It can help reduce the number of parentheses in complex expressions.
use loophp\combinator\Combinators;

$a = Combinators::A();
$strlen = 'strlen';
$string = 'hello world';

// Instead of $strlen($string)
echo $a($strlen)($string); // Outputs: 11
```</details>

<details>
<summary>B (Bluebird) Combinator</summary>

*   **Lambda:** `λabc.a(bc)`
*   **Purpose:** Function composition. It takes two functions, `a` and `b`, and a value `c`, and applies `a` to the result of `b` applied to `c`. This is `.` in Haskell.

```php
use loophp\combinator\Combinators;

$b = Combinators::B();
$addOne = fn(int $x): int => $x + 1;
$multiplyByTwo = fn(int $x): int => $x * 2;

// Create a new function: multiply by two, then add one.
$composed = $b($addOne)($multiplyByTwo);

echo $composed(5); // Outputs: 11 (which is 1 + (2 * 5))
Blackbird Combinator
  • Lambda: λabcd.a(bcd)
  • Purpose: Extended function composition for three functions: a(b(c(d))).
use loophp\combinator\Combinators;

$blackbird = Combinators::Blackbird();
$wrapInP = fn(string $s): string => "<p>$s</p>";
$toUpper = 'strtoupper';
$addExclamation = fn(string $s): string => "$s!";

$format = $blackbird($wrapInP)($toUpper)($addExclamation);

echo $format('hello'); // Outputs: <p>HELLO!</p>
C (Cardinal) Combinator
  • Lambda: λabc.acb
  • Purpose: Flips arguments. It takes a function a and two arguments b and c, and applies a with c as the first argument and b as the second. This is flip in Haskell.
use loophp\combinator\Combinators;

$c = Combinators::C();
$divide = fn(int $x, int $y): float => $x / $y;

$flippedDivide = $c($divide);

echo $flippedDivide(2)(10); // Outputs: 5 (same as $divide(10, 2))
```</details>

<details>
<summary>D (Dove) Combinator</summary>

*   **Lambda:** `λabcd.ab(cd)`
*   **Purpose:** Composes two functions and their arguments: `a(b)(c(d))`.

```php
use loophp\combinator\Combinators;

$d = Combinators::D();
$add = fn(int $x): callable => fn(int $y): int => $x + $y;
$multiply = fn(int $x): callable => fn(int $y): int => $x * $y;

// Calculates ( (2 + 3) + (4 * 5) ) using curried functions
echo $d($add)($add(2)(3))($multiply(4))(5); // Outputs: 25
E (Eagle) Combinator
  • Lambda: λabcde.ab(cde)
  • Purpose: Five-argument composition, useful for deeply nested function calls: a(b)(c(d(e))).
use loophp\combinator\Combinators;

$e = Combinators::E();
$add = fn(int $x): callable => fn(int $y): int => $x + $y;
$multiplyByTwo = fn(int $x): int => $x * 2;
$addOne = fn(int $x): int => $x + 1;

// Equivalent to: $add(10)(($multiplyByTwo($addOne(5))))
echo $e($add)(10)($multiplyByTwo)($addOne)(5); // Outputs: 22
F (Finch) Combinator
  • Lambda: λabc.cba
  • Purpose: Reverses the application order. Applies c to b, and then to a.
use loophp\combinator\Combinators;

$f = Combinators::F();
$concat = fn(string $a): callable => fn(string $b): string => $a . $b;

// Instead of $concat(' world')('hello')
echo $f('hello')(' world')($concat); // Outputs: " worldhello"
G (Goldfinch) Combinator
  • Lambda: λabcd.ad(bc)
  • Purpose: Applies arguments in a unique order: a(d)(b(c)).
use loophp\combinator\Combinators;

$g = Combinators::G();
$merge = fn(array $a): callable => fn(array $b): array => array_merge($a, $b);
$mapToUpper = fn(array $a): array => array_map('strtoupper', $a);

// Equivalent to $merge(['d', 'e'])(($mapToUpper(['a', 'b', 'c'])))
$result = $g($merge)($mapToUpper)(['a', 'b', 'c'])(['d', 'e']);
print_r($result); // Outputs: Array ( [0] => A [1] => B [2] => C [3] => d [4] => e )
H (Hummingbird) Combinator
  • Lambda: λabc.abcb
  • Purpose: Applies a three-argument function, but uses the second argument twice: a(b)(c)(b).
use loophp\combinator\Combinators;

$h = Combinators::H();
$wrap = fn(string $tag): callable => fn(string $content): callable => fn(string $closingTag): string => "<$tag>$content</$closingTag>";

// Uses 'div' as both the opening and closing tag.
$wrapInDiv = $h($wrap)('div');

echo $wrapInDiv('Hello'); // Outputs: <div>Hello</div>
I (Idiot) Combinator
  • Lambda: λa.a
  • Purpose: The identity function. It returns whatever argument it receives. This is id in Haskell.
use loophp\combinator\Combinators;

$i = Combinators::I();

echo $i('Hello World'); // Outputs: Hello World
echo $i(42);            // Outputs: 42
J (Jay) Combinator
  • Lambda: λabcd.ab(adc)
  • Purpose: A complex combinator useful for specific recursive or state-passing scenarios: a(b)(a(d)(c)).
use loophp\combinator\Combinators;

$j = Combinators::J();
$log = fn(string $prefix): callable => fn(string $message): string => "[$prefix] $message";

// Creates a nested log message
// Equivalent to $log('INFO')($log('USER')('action'))
echo $j($log)('INFO')('action')('USER'); // Outputs: [INFO] [USER] action```
</details>

<details>
<summary>K (Kestrel) Combinator</summary>

*   **Lambda:** `λab.a`
*   **Purpose:** The constant function. It takes two arguments and always returns the first. This is `const` in Haskell.

```php
use loophp\combinator\Combinators;

$k = Combinators::K();
$alwaysReturns5 = $k(5);

echo $alwaysReturns5('foo'); // Outputs: 5
echo $alwaysReturns5(123);   // Outputs: 5
Ki (Kite) Combinator
  • Lambda: λab.b
  • Purpose: The opposite of the Kestrel. It takes two arguments and always returns the second.
use loophp\combinator\Combinators;

$ki = Combinators::Ki();
$getSecond = $ki('ignored');

echo $getSecond('hello'); // Outputs: hello
echo $getSecond(true);    // Outputs: 1 (for true)
L (Lark) Combinator
  • Lambda: λab.a(bb)
  • Purpose: Applies function a to the result of function b applied to itself. This requires b to be a function that can meaningfully accept itself as an argument.
use loophp\combinator\Combinators;

$l = Combinators::L();
$inspectType = fn(mixed $arg): string => "Argument is of type: " . gettype($arg);
$selfApplicable = fn(callable $c): string => "I am a function.";

// Equivalent to $inspectType($selfApplicable($selfApplicable))
echo $l($inspectType)($selfApplicable); // Outputs: Argument is of type: string
M (Mockingbird) Combinator
  • Lambda: λa.aa
  • Purpose: Self-application (also known as the ω combinator). It applies its single argument (which must be a function) to itself.
use loophp\combinator\Combinators;

$m = Combinators::M();
$describe = fn(callable $c): string => "This is a closure.";

// Applies the $describe function to itself.
echo $m($describe); // Outputs: This is a closure.
O (Owl) Combinator
  • Lambda: λab.b(ab)
  • Purpose: A peculiar form of composition, b(a(b)). Useful in specific recursive algorithms or data structure manipulations.
use loophp\combinator\Combinators;

$o = Combinators::O();
// A curried function to prepend to an array
$prepend = fn(mixed $item): callable => fn(array $list): array => [$item, ...$list];
$reverse = 'array_reverse';

// Equivalent to $reverse($prepend(3)([1, 2])) -> $reverse([3, 1, 2])
$result = $o($prepend)([1, 2])($reverse)(3);
print_r($result); // Outputs: Array ( [0] => 2 [1] => 1 [2] => 3 )
Omega (Ω) Combinator
  • Lambda: λa.(aa)(aa)
  • Purpose: The divergent combinator. It is constructed from the Mockingbird (M) as MM. When applied to any function, it creates an infinitely recursive call that will exhaust memory. Do not run this code.
// This will cause a fatal error due to infinite recursion.
// $omega = Combinators::Omega();
// $omega(fn($x) => $x);```
</details>

<details>
<summary>Phoenix Combinator</summary>

*   **Lambda:** `λabcd.a(bd)(cd)`
*   **Purpose:** Distributes an argument `d` across two functions `b` and `c`, then combines the results with function `a`.

```php
use loophp\combinator\Combinators;

$phoenix = Combinators::Phoenix();
$add = fn(int $x): callable => fn(int $y): int => $x + $y;
$multiplyByTwo = fn(int $x): int => $x * 2;
$multiplyByThree = fn(int $x): int => $x * 3;

// Calculates (2 * 10) + (3 * 10)
$calculate = $phoenix($add)($multiplyByTwo)($multiplyByThree);

echo $calculate(10); // Outputs: 50
Psi (Ψ) Combinator
  • Lambda: λabcd.a(bc)(bd)
  • Purpose: Another distribution combinator, but this one distributes a function b over two arguments c and d.
use loophp\combinator\Combinators;

$psi = Combinators::Psi();
$makePair = fn(string $a): callable => fn(string $b): string => "($a, $b)";
$getName = fn(array $user): string => $user['name'];
$getEmail = fn(array $user): string => $user['email'];
$user = ['name' => 'John', 'email' => 'john@example.com'];

// Equivalent to $makePair($getName($user))($getEmail($user))
echo $psi($makePair)($getName)($getEmail)($user); // Outputs: (John, john@example.com)
Q (Queer) Combinator
  • Lambda: λabc.b(ac)
  • Purpose: A different form of composition, applying b to the result of a applied to c.
use loophp\combinator\Combinators;

$q = Combinators::Q();
$addOne = fn(int $x): int => $x + 1;
$multiplyByTwo = fn(int $x): int => $x * 2;

// Equivalent to $multiplyByTwo($addOne(5))
$composed = $q($addOne)($multiplyByTwo);

echo $composed(5); // Outputs: 12
R (Robin) Combinator
  • Lambda: λabc.bca
  • Purpose: Reorders arguments for a two-argument function b.
use loophp\combinator\Combinators;

$r = Combinators::R();
$divide = fn(int $x): callable => fn(int $y): float => $x / $y;

// Equivalent to $divide(10)(2)
echo $r(2)($divide)(10); // Outputs: 5
S (Starling) Combinator
  • Lambda: λabc.ac(bc)
  • Purpose: The substitution combinator. It applies a third argument c to both a and b, then applies the result of a(c) to the result of b(c). It is fundamental to combinatory logic.
use loophp\combinator\Combinators;

$s = Combinators::S();
$add = fn(int $x): callable => fn(int $y): int => $x + $y;
$square = fn(int $x): int => $x * $x;

// Equivalent to $add($x)($square($x)) where $x is 3. So 3 + (3*3)
echo $s($add)($square)(3); // Outputs: 12
S' (S Prime) Combinator
  • Lambda: λabc.a(bc)c
  • Purpose: A variation of the Starling that provides the final argument c to the outer function a as well. Useful for functions that need both the result of an operation and the original value, such as logging or debugging.
use loophp\combinator\Combinators;

$s_ = Combinators::S_();
$log = fn(string $result): callable => fn(string $original): string => "Input '$original' produced '$result'";
$transform = 'strtoupper';

// Equivalent to $log(strtoupper('hello'))('hello')
$logTransformation = $s_($log)($transform);

echo $logTransformation('hello'); // Outputs: Input 'hello' produced 'HELLO'
S₂ (S-Two) Combinator
  • Lambda: λabcd.a((bd)(cd))
  • Purpose: Applies two different functions b and c to the same data d, and then combines their results with a third function a. In Haskell, this is similar to liftA2.
use loophp\combinator\Combinators;

$s2 = Combinators::S2();
$combine = fn(int $product): callable => fn(int $sum): string => "Sum: $sum, Product: $product";
$sum = fn(int $x): callable => fn(int $y): int => $x + $y;
$product = fn(int $x): callable => fn(int $y): int => $x * $y;
$value = 3;

// This is a bit mind-bending. The lambda is a((bd)(cd)).
// Let's find a better example. `a` must take one argument.
$format = fn(int $result): string => "Final result is: $result";
$multiply = fn(int $x): callable => fn(int $y): int => $x * $y;
$addOne = fn(int $x): int => $x + 1;
$d = 5;

// Equivalent to $format($multiply(5)($addOne(5))) -> $format(5 * 6)
echo $s2($format)($multiply)($addOne)($d); // Outputs: Final result is: 30
T (Thrush) Combinator
  • Lambda: λab.ba
  • Purpose: Reverse application. Applies function b to argument a. This is (&) in Haskell and is useful for data-last programming styles.
use loophp\combinator\Combinators;

$t = Combinators::T();
$multiplyByTwo = fn(int $x): int => $x * 2;

// Instead of $multiplyByTwo(5)
echo $t(5)($multiplyByTwo); // Outputs: 10
```</details>

<details>
<summary>U (Turing) Combinator</summary>

*   **Lambda:** `λab.b(aab)`
*   **Purpose:** A fixed-point combinator that can hold state. `b` is applied to `a` applied to itself applied to `b`.

```php
use loophp\combinator\Combinators;

$u = Combinators::U();
$factorialGenerator = static fn(callable $f): callable =>
    static fn(int $n): int => (0 === $n) ? 1 : $n * $f($f)($n - 1);

$factorial = $u($factorialGenerator);

echo $factorial(5); // Outputs: 120
V (Vireo) Combinator
  • Lambda: λabc.cab
  • Purpose: Argument swapping. Given c, a, b, it calls c(a)(b).
use loophp\combinator\Combinators;

$v = Combinators::V();
$str_replace = 'str_replace';
$search = 'foo';
$replace = 'bar';

// Creates a function that replaces 'foo' with 'bar' in a given subject string.
$replaceFoo = $v($search)($replace)($str_replace);

echo $replaceFoo('this is foo'); // Outputs: this is bar
W (Warbler) Combinator
  • Lambda: λab.abb
  • Purpose: Duplicates an argument. It applies function a to argument b twice.
use loophp\combinator\Combinators;

$w = Combinators::W();
$add = fn(int $x): callable => fn(int $y): int => $x + $y;

// Equivalent to $add(5)(5)
echo $w($add)(5); // Outputs: 10
Y & Z (Fixed-Point) Combinators
  • Purpose: The Y and Z combinators are used to achieve recursion in languages that do not have built-in recursive syntax. They are "fixed-point" finders for functions. The Z combinator is a variant that works in strict (eagerly evaluated) languages like PHP without causing infinite loops during construction.
use loophp\combinator\Combinators;
use Closure;

// A "generator" function that describes one step of the factorial calculation.
// It takes the recursive function ($fact) as an argument.
$factorialGenerator = static fn (Closure $fact): Closure =>
    static fn (int $n): int => (0 === $n) ? 1 : ($n * $fact($n - 1));

// The Z combinator creates a recursive factorial function from the generator.
$factorial = Combinators::Z()($factorialGenerator);

echo $factorial(6); // Outputs: 720

// The Y combinator can also be used, as this library implements it in a way
// that is safe for eager evaluation.
$fibonacciGenerator = static fn (Closure $fibo): Closure =>
    static fn (int $n): int => ($n <= 1) ? $n : $fibo($n - 1) + $fibo($n - 2);

$fibonacci = Combinators::Y()($fibonacciGenerator);

echo $fibonacci(10); // Outputs: 55

Suggested reading and resources

Contributing

Feel free to contribute by sending pull requests. We are a usually very responsive team and we will help you going through your pull request from the beginning to the end.

For some reasons, if you can't contribute to the code and willing to help, sponsoring is a good, sound and safe way to show us some gratitude for the hours we invested in this package.

Sponsor me on Github and/or any of the contributors.

Thanks

Authors

Changelog

See CHANGELOG.md for a changelog based on git commits. For more detailed changelogs, please check the release changelogs.