Functional PHP

[)roi(]

Executive Member
Joined
Apr 15, 2005
Messages
6,282
Reaction score
405
Location
I've recently been drawn into a project that has selected to use PHP for a portion of the build; which means I had a chance to relook at what's possible i.r.o. PHP and functional code; naturally that meant I'd need some of the common algebras (Functor, Applicative, Monad) and a few types that exploit these, for example:
  • Option
  • Either
  • Validation
  • ReaderMonad
  • ...

Well colour me surprised; this has been far easier to implement than I imagined it would be, and the constructs and combinators (curry, partial, k combinator, etc.) aren't looking too unfriendly at all, and surprisingly easy to use.

So let me know if there's any interested for FP on PHP; in the interim I could possibly share some of the code for this (work in progress), and also maybe even a few examples of how to use these.
 
Update; I have completed most of the common data types, combinators and helper functions for base PHP types. At least enough that most the typical code scenarios are covered:

Here for interest's sake is an implementation for the Either sum type:
PHP:
<?php

namespace Endo\Data;

use Endo\Core\Module;
use Endo\Lib\Strings;
use Endo\Typeclass\IMonad;

/**
* Either Monad
*/
class Either extends Module implements IMonad {
    private $value;
    private $isRight;

    private function __construct($value, $isRight) {
        $this->value = $value;
        $this->isRight = $isRight;
    }

    protected static function __left($value): Either {
        return new Either($value, false);
    }

    protected static function __right($value): Either {
        return new Either($value, true);
    }

    public function map(callable $f): Either {
        return $this->isRight ? self::right($f($this->value)) : $this;
    }

    public static function pure($a): Either {
        return self::right($a);
    }

    public function apply($a): Either {
        return $this->flatmap(function ($f) use ($a) {
            return $a->map(function ($x) use ($f) {
                return $f($x);
            });
        });
    }

    public function liftA2($a, $b): Either {
        return $this->apply($a)->apply($b);
    }

    public function liftA3($a, $b, $c): Either {
        return $this->apply($a)->apply($b)->apply($c);
    }

    public function liftA4($a, $b, $c, $d): Either {
        return $this->apply($a)->apply($b)->apply($c)->apply($d);
    }

    public function liftA5($a, $b, $c, $d, $e): Either {
        return $this->apply($a)->apply($b)->apply($c)->apply($d)->apply($e);
    }

    public function liftA6($a, $b, $c, $d, $e, $f): Either {
        return $this->apply($a)->apply($b)->apply($c)->apply($d)->apply($e)->apply($f);
    }

    public function liftA7($a, $b, $c, $d, $e, $f, $g): Either {
        return $this->apply($a)->apply($b)->apply($c)->apply($d)->apply($e)->apply($f)->apply($g);
    }

    public function liftA8($a, $b, $c, $d, $e, $f, $g, $h): Either {
        return $this->apply($a)->apply($b)->apply($c)->apply($d)->apply($e)->apply($f)->apply($g)->apply($h);
    }

    public function liftA9($a, $b, $c, $d, $e, $f, $g, $h, $i): Either {
        return $this->apply($a)->apply($b)->apply($c)->apply($d)->apply($e)->apply($f)->apply($g)->apply($h)->apply($i);
    }

    public function flatmap(callable $f): Either {
        return $this->isRight ? $f($this->value) : $this;
    }

    public function getValue() {
        return $this->value;
    }

    public function match(callable $left, callable $right): void{
        $this->isRight ? $right($this->getValue()) : $left($this->getValue());
    }

    public function isRight(): bool {
        return $this->isRight;
    }

    protected static function __extract($eitherValue) {
        return $eitherValue->getValue();
    }

    protected static function __fromOption($leftValue, Option $optionValue): Either {
        return Option::isSome($optionValue) ? Either::right(Option::extract($optionValue)) : Either::left($leftValue);
    }

    public function __toString(): string {
        return Strings::log($this->isRight ? "Either::right" : "Either::left", $this->value);
    }
}

?>

Example of how we can use the Either for exception handling for some SQL helper functions:
Either's monadic process binding using flatMap:
PHP:
<?php
namespace Endo\Lib;

use Endo\Core\Module;
use Endo\Data\Either;
use Endo\lib\ƒ;

class SQL extends Module {

    public static function mysqlQuery(string $sql) {
        return SQL::connect()
            ->flatMap(SQL::query($sql))
            ->flatMap(SQL::close());
    }

    public static function exec2JSON(string $sql, string $root): void{
        SQL::mysqlQuery($sql)->match(SQL::encodeJSON($root), SQL::encodeJSON($root));
    }

    public static function encodeJSON(string $root) {
        return function ($success) use ($root) {
            echo json_encode(array($root => $success));
        };
    }

    public static function connect(): Either {
        return SQL::__connect(\Config::DSN, \Config::DATABASE, \Config::HOST, \Config::USERNAME, \Config::PASSWORD);
    }

    protected static function __connect(string $dsn, string $database, string $host, string $username, string $password): Either {
        return ƒ::tryEither(function () use ($dsn, $database, $host, $username, $password) {
            $mysqlpdo = new \PDO("$dsn:dbname=$database;host=$host", $username, $password);
            return $mysqlpdo;
        });
    }

    public static function query($sql): callable {
        return function ($connection) use ($sql) {
            return ƒ::tryEither(function () use ($connection, $sql) {
                $result = $connection->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                if (!$result) {
                    throw new \Exception($connection->errorInfo()[2], 1);
                }
                return [$connection, $result];
            });
        };
    }

    public static function close(): callable {
        return function ($queryoutput) {
            return ƒ::tryEither(function () use ($queryoutput) {
                $connection = $queryoutput[0];
                $result = $queryoutput[1];
                $connection = null;
                return $result;
            });
        };
    }
}
?>

// try catch wrapper for Either type
<?php

namespace Endo\Lib;

use Endo\Core\Module;
use Endo\Data\Either;
use Endo\Data\Option;
use Endo\Data\Validation;

class ƒ extends Module {

    protected static function __tryEither(callable $block): Either{
        set_error_handler("exception_error_handler");
        try {
            return Either::right($block());
        } catch (\Exception | \PDOException | ErrorException $e) {
            return Either::left([$e->getMessage()]);
        } finally {
            restore_error_handler();
        }
    }
}

...and this all results in a fairly simple solution for firing off SQL queries or stored procedures.
Note: example is of two simple web APIs using the fat-free framework.
PHP:
use Endo\Lib\SQL;

$f3 = require 'lib/base.php';
$f3->config('config.ini');

$f3->route('GET /api/actor', function ($f3) {
    SQL::exec2JSON("select * from actor", "actors");
});

$f3->route('GET /api/actor/@id', function ($f3) {
    $id = $f3->get('PARAMS.id');
    SQL::exec2JSON("select * from actor where actor_id='" . $id . "'", "actors");
});

Anyway if any anyone is interested to have a full copy of the functional data type implementation let me know; the following parts are code complete:
  • Data types: Either, Validation, Option, Reader, Identity
  • Optics: Lens, Prism
  • Combinators: Pipe, Composition, Argument Flip, k-combinator (also known as const), id, nop, Curry, Partial application
  • ...and a few other helper functions for String, Array, Math, Logic, Object, ...
 
Last edited:
Top
Sign up to the MyBroadband newsletter
X