fp4php/functional

PHP Functional Programming library

v6.0.0 2023-05-22 14:30 UTC

README

PHP Functional Programming library. Monads and common use functions.

psalm level psalm type coverage phpunit coverage

Documentation

Installation

Composer

Supported installation method is via composer:

$ composer require fp4php/functional

Psalm integration

Please refer to the fp4php/functional-psalm-plugin repository.

Overview

Typesafe and concise.

Powerful combination: Collections + Option monad.

<?php

use Fp\Collections\ArrayList;
use Fp\Functional\Option\Option;

use function Fp\Evidence\of;
use function Fp\Evidence\proveString;

class PgSqlCurrencyArrayType extends Type
{
    public function convertToDatabaseValue($value, AbstractPlatform $platform): string
    {
        $currencies = Option::fromNullable($value)
            ->filter(is_iterable(...))
            ->getOrElse([]);

        return ArrayList::collect($currencies)
            ->flatMap(of(Currency::class))
            ->map(fn(Currency $currency) => $currency->getCurrencyCode())
            ->mkString('{', ',', '}');
    }

    /**
     * @return ArrayList<Currency>
     */
    public function convertToPHPValue($value, AbstractPlatform $platform): ArrayList
    {
        $csv = Option::fromNullable($value)
            ->flatMap(proveString(...))
            ->map(fn(string $pgSqlArray) => trim($pgSqlArray, '{}'))
            ->getOrElse('');

        return ArrayList::collect(explode(',', $csv))
            ->filterMap($this->parseCurrency(...));
    }

    /**
     * @return Option<Currency>
     */
    public function parseCurrency(string $currencyCode): Option
    {
        return Option::try(fn() => Currency::of($currencyCode));
    }
}

Examples

  • Type safety
<?php

use Fp\Collections\NonEmptyLinkedList;

/**
 * Inferred type is NonEmptyLinkedList<1|2|3>
 */
$collection = NonEmptyLinkedList::collectNonEmpty([1, 2, 3]);

/**
 * Inferred type is NonEmptyLinkedList<int>
 * 
 * Literal types are dropped after map transformation,
 * but NonEmpty collection prefix has been kept
 */
$mappedCollection = $collection->map(fn($elem) => $elem - 1);

/**
 * Inferred type is LinkedList<positive-int>
 * NonEmpty prefix has been dropped
 */
$filteredCollection = $mappedCollection->filter(fn(int $elem) => $elem > 0);
<?php

use Tests\Mock\Foo;
use Tests\Mock\Bar;
use Fp\Collections\NonEmptyArrayList;

$source = [new Foo(1), null, new Bar(2)];

/**
 * Inferred type is ArrayList<Foo|Bar>
 * Null type was removed
 * NonEmpty prefix was removed
 */
$withoutNulls = NonEmptyArrayList::collectNonEmpty($source)
    ->filter(fn(Foo|Bar|null $elem) => null !== $elem);

/**
 * Inferred type is ArrayList<Foo>
 * Bar type was removed
 */
$onlyFoos = $withoutNulls->filter(fn($elem) => $elem instanceof Foo);
  • Covariance
<?php

use Fp\Collections\NonEmptyLinkedList;

class User {}
class Admin extends User {}

/**
* @param NonEmptyLinkedList<User> $collection
*/
function acceptUsers(NonEmptyLinkedList $collection): void {}

/** 
 * @var NonEmptyLinkedList<Admin> $collection 
 */
$collection = NonEmptyLinkedList::collectNonEmpty([new Admin()]);

/**
 * You can pass collection of admins instead of users
 * Because of covariant template parameter
 */
acceptUsers($collection);
  • Immutability
<?php

use Fp\Collections\LinkedList;

$originalCollection = LinkedList::collect([1, 2, 3]);

/**
 * $originalCollection won't be changed
 */
$prependedCollection = $originalCollection->prepended(0);

/**
 * $prependedCollection won't be changed
 */
$mappedCollection = $prependedCollection->map(fn(int $elem) => $elem + 1);
  • Null safety
<?php

use Fp\Functional\Option\Option;
use Fp\Collections\ArrayList;

/**
 * @var ArrayList<int> $collection 
 */
$collection = getCollection();

/**
 * @return Option<float>
 */
function div(int $a, int $b): Option
{
    return Option::when(0 !== $b, fn() => $a / $b);
}

/**
 * It's possible there is no first collection element above zero
 * or divisor is zero.
 *
 * In this case the execution will short circuit (stop)
 * and no Null Pointer Exception will be thrown.
 */
$collection
    ->first(fn(int $elem) => $elem > 0)
    ->map(fn(int $elem) => $elem + 1)
    ->flatMap(fn(int $elem) => div($elem, $elem - 1))
    ->getOrElse(0)
<?php

use Tests\Mock\Foo;
use Fp\Functional\Option\Option;

use function Fp\Evidence\proveTrue;
use function Fp\Evidence\proveNonEmptyList;

/**
 * Inferred type is Option<Foo> 
 */ 
$maybeFooMaybeNot = Option::do(function() use ($untrusted) {
    // If $untrusted is not null then bind this value to $notNull
    $notNull = yield Option::fromNullable($untrusted);
 
    // If $notNull is non-empty-list<Tests\Mock\Foo> then bind this value to $nonEmptyListOfFoo 
    $nonEmptyList = yield proveNonEmptyList($notNull, of(Foo::class));

    // Continue computation if $nonEmptyList contains only one element
    yield proveTrue(1 === count($nonEmptyList));

    // I'm sure it's Foo object
    return $nonEmptyList[0];
});

// Inferred type is Tests\Mock\Foo
$foo = $maybeFooMaybeNot->getOrCall(fn() => new Foo(0));

Contribution

Build documentation

  1. Install dependencies
$ sudo apt install pandoc
  1. Generate doc from src
$ make