Skip to content

PHP Functional Programming library. Monads, common use functions and generic collections.

License

Notifications You must be signed in to change notification settings

fp4php/functional

Repository files navigation

Functional PHP

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