Code Monkey home page Code Monkey logo

php-functional's Introduction

PHP Functional

Build Status Test Coverage Maintainability

Introduction

Functional programing is a fascinating concept. The purpose of this library is to explore Functors, Applicative Functors and Monads in OOP PHP, and provide examples of real world use case.

Monad types available in the project:

  • State Monad
  • IO Monad
  • Free Monad
  • Either Monad
  • Maybe Monad
  • Reader Monad
  • Writer Monad

Exploring functional programing space I noticed that working with primitive values from PHP is very hard and complicates implementation of many functional structures. To simplify this experience, set of higher order primitives is introduced in library:

  • Num
  • Sum
  • Product
  • Stringg
  • Listt (a.k.a List Monad, since list is a protected keyword in PHP)

Applications

Known applications of this project

  • Algorithm W implemented in PHP based on Martin Grabmüller work.

Installation

composer require widmogrod/php-functional

Development

This repository follows semantic versioning concept. If you want to contribute, just follow CONTRIBUTING.md

Testing

Quality assurance is brought to you by:

  • PHPUnit
  • Eris - QuickCheck and property-based testing tools to the PHP and PHPUnit ecosystem.
  • PHP-CS-Fixer - A tool to automatically fix PHP coding standards issues
composer test
composer fix 

Use Cases

You can find more use cases and examples in the example directory.

NOTE: Don't be confused when browsing thought examples you will see phrase like "list functor" and in this library you will see Widmogrod\Primitive\Listt. Monad is Functor and Applicative. You could say that Monad implements Functor and Applicative.

List Functor

use Widmogrod\Functional as f;
use Widmogrod\Primitive\Listt;

$list = f\fromIterable([
   ['id' => 1, 'name' => 'One'],
   ['id' => 2, 'name' => 'Two'],
   ['id' => 3, 'name' => 'Three'],
]);

$result = $list->map(function($a) {
    return $a['id'] + 1;
});

assert($result === f\fromIterable([2, 3, 4]));

List Applicative Functor

Apply function on list of values and as a result, receive list of all possible combinations of applying function from the left list to a value in the right one.

[(+3),(+4)] <*> [1, 2] == [4, 5, 5, 6]
use Widmogrod\Functional as f;
use Widmogrod\Primitive\Listt;

$listA = f\fromIterable([
    function($a) {
        return 3 + $a;
    },
    function($a) {
        return 4 + $a;
    },
]);
$listB = f\fromIterable([
    1, 2
]);

$result = $listA->ap($listB);

assert($result === f\fromIterable([4, 5, 5, 6]));

Maybe Monoid

Using Maybe as an instance of Monoid simplifies concat and reduce operations by using Maybe's abstraction over potentially missing values. See an example of constructing a person's full name from first, middle, and last without having to explicitly check if each part exists.

Maybe and List Monad

Extracting from a list of uneven values can be tricky and produce nasty code full of if (isset) statements. By combining List and Maybe Monad, this process becomes simpler and more readable.

use Widmogrod\Monad\Maybe;
use Widmogrod\Primitive\Listt;

$data = [
    ['id' => 1, 'meta' => ['images' => ['//first.jpg', '//second.jpg']]],
    ['id' => 2, 'meta' => ['images' => ['//third.jpg']]],
    ['id' => 3],
];

// $get :: String a -> Maybe [b] -> Maybe b
$get = function ($key) {
    return f\bind(function ($array) use ($key) {
        return isset($array[$key])
            ? Maybe\just($array[$key])
            : Maybe\nothing();
    });
};

$result = f\fromIterable($data)
    ->map(Maybe\maybeNull)
    ->bind($get('meta'))
    ->bind($get('images'))
    ->bind($get(0));

assert(f\valueOf($result) === ['//first.jpg', '//third.jpg', null]);

Either Monad

In php world, the most popular way of saying that something went wrong is to throw an exception. This results in nasty try catch blocks and many of if statements. Either Monad shows how we can fail gracefully without breaking the execution chain and making the code more readable. The following example demonstrates combining the contents of two files into one. If one of those files does not exist the operation fails gracefully.

use Widmogrod\Functional as f;
use Widmogrod\Monad\Either;

function read($file)
{
    return is_file($file)
        ? Either\Right::of(file_get_contents($file))
        : Either\Left::of(sprintf('File "%s" does not exists', $file));
}

$concat = f\liftM2(
    read(__DIR__ . '/e1.php'),
    read('aaa'),
    function ($first, $second) {
        return $first . $second;
    }
);

assert($concat instanceof Either\Left);
assert($concat->extract() === 'File "aaa" does not exists');

IO Monad

Example usage of IO Monad. Read input from stdin, and print it to stdout.

use Widmogrod\Monad\IO as IO;
use Widmogrod\Functional as f;

// $readFromInput :: Monad a -> IO ()
$readFromInput = f\mcompose(IO\putStrLn, IO\getLine, IO\putStrLn);
$readFromInput(Monad\Identity::of('Enter something and press <enter>'))->run();

Writer Monad

The Writer monad is useful to keep logs in a pure way. Coupled with filterM for example, this allows you to know exactly why an element was filtered.

use Widmogrod\Monad\Writer as W;
use Widmogrod\Functional as f;
use Widmogrod\Primitive\Stringg as S;

$data = [1, 10, 15, 20, 25];

$filter = function($i) {
    if ($i % 2 == 1) {
        return W::of(false, S::of("Reject odd number $i.\n"));
    } else if($i > 15) {
      return W::of(false, S::of("Reject $i because it is bigger than 15\n"));
    }

    return W::of(true);
};

list($result, $log) = f\filterM($filter, $data)->runWriter();

Reader Monad

The Reader monad provides a way to share a common environment, such as configuration information or class instances, across multiple functions.

use Widmogrod\Monad\Reader as R;
use Widmogrod\Functional as f;

function hello($name) {
    return "Hello $name!";
}

function ask($content)
{
    return R::of(function($name) use($content) {
        return $content.
               ($name == 'World' ? '' : ' How are you?');
    });
}

$r = R\reader('hello')
      ->bind('ask')
      ->map('strtoupper');

assert($r->runReader('World') === 'HELLO WORLD!')

assert($r->runReader('World') === 'HELLO GILLES! HOW ARE YOU?')

Free Monad in PHP

Imagine that you first write business logic and don't care about implementation details like:

  • how and from where get user discounts
  • how and where save products in basket
  • and more ...

When your business logic is complete, then you can concentrate on those details.

Free monad enables you to do exactly that, and more:

  • Write business logic first
  • Write your own DLS (domain specific language)
  • Decouple implementation from interpretation.

Echo program

Example Free Monad example of echo program can be found here:

DSL for BDD tests

Example that use Free Monad to creates simple DSL (Domain Specific Language) to define BDD type of framework:

$state = [
    'productsCount' => 0,
    'products' => [],
];

$scenario =
    Given('Product in cart', $state)
        ->When("I add product 'coca-cola'")
        ->When("I add product 'milk'")
        ->Then("The number of products is '2'");

$result = $scenario->Run([
    "/^I add product '(.*)'/" => function ($state, $productName) {
        $state['productsCount'] += 1;
        $state['products'][] = $productName;

        return $state;
    },
], [
    "/^The number of products is '(\d+)'/" => function ($state, int $expected) {
        return $state['productsCount'] === $expected;
    },
]);

Free Monad Calculator example

Example of a DSL for a naive calculator that is implemented by using FreeMonad.

Free monad can be interpreted as a real calculator or calculation formatter a.k.a. pretty printer. Additional thing that I wanted to tackle was a Free Monad Optimisation.

Considering that Free Monad is like AST, question arose in my mind - can I travers it and update it to simplify computation? Hot to do it? What are limitation of Free Monad? Calculator example is an outcome of those questions.

$calc = mul(
    sum(int(2), int(1)),
    sum(int(2), int(1))
);

$expected = '((2+1)^2)';

$result = foldFree(compose(interpretPrint, optimizeCalc), $calc, Identity::of);
$this->assertEquals(
    Identity::of(Stringg::of($expected)),
    $result
);

Haskell do notation in PHP

Why Haskell's do notation is interesting?

In Haskell is just an "syntax sugar" and in many ways is not needed, but in PHP control flow of monads can be hard to track.

Consider example, that use only chaining bind() and compare it to the same version but with do notation in PHP.

Control flow without do notation

$result = Identity::of(1)
    ->bind(function ($a) {
        return Identity::of(3)
            ->bind(function ($b) use ($a) {
                return Identity::of($a + $b)
                    ->bind(function ($c) {
                        return Identity::of($c * $c);
                    });
            });
    });

$this->assertEquals(Identity::of(16), $result);

Control flow with do notation

$result = doo(
    let('a', Identity::of(1)),
    let('b', Identity::of(3)),
    let('c', in(['a', 'b'], function (int $a, int $b): Identity {
        return Identity::of($a + $b);
    })),
    in(['c'], function (int $c): Identity {
        return Identity::of($c * $c);
    })
);

assert($result === Identity::of(16));

Everyone needs to judge by itself, but in my opinion do notationimprove readability of code in PHP.

Book Functional PHP by Gilles Crettenand

In recently published book Functional PHP by Gilles Crettenand, you can learn more applications of widmogrod/php-functional, see how it compares to other projects and how in an effortless way apply functional thinking in daily work.

Buy the book at PacktPub

References

Here links to their articles/libraries that help me understood the domain:

php-functional's People

Contributors

eduardogr avatar ilario-pierbattista avatar krtek4 avatar mattjmattj avatar nekufa avatar nick-zh avatar safareli avatar tpl0ch avatar widmogrod avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

php-functional's Issues

Writer and Reader monads

Hey,

I have an implementation of the Writer and Reader monads that are using the interfaces you defined.

At the time, I didn't write any tests however.

Would you be interested in a PR for those two monads ?

Explanation needed concerning utility functions and currying

Hey,

I am probably missing something, but what is the poing of creating an inner function, currying it and then using call_user_func_array to finally call the result in most utility functions ?

For example the map implementation ( https://github.com/widmogrod/php-functional/blob/master/src/Functional/functions.php#L354 )

function map(callable $transformation = null, Functor $value = null)
{
    return call_user_func_array(curryN(2, function (callable $transformation, Functor $value) {
        return $value->map($transformation);
    }), func_get_args());
}

Isn't this strictly equivalent to :

function map(callable $transformation = null, Functor $value = null)
{
    return $value->map($transformation);
}

What am I missing here ?

Thanks !

Try to use FreeMonad to implement Haskell do notation in PHP

Why Haskell do notation is interesting?

In Haskell is just an "syntax sugar" and in many ways is not needed, but in PHP sometimes control flow of monads can be hard to track.

Consider example that use only chaining and compare it to the same version but with do notation in PHP.

Control flow without do notation

$result = Identity::of(1)
    ->bind(function ($a) {
        return Identity::of(3)
            ->bind(function ($b) use ($a) {
                return Identity::of($a + $b)
                    ->bind(function ($c) {
                        return Identity::of($c * $c);
                    });
            });
    });

$this->assertEquals(Identity::of(16), $result);
Control flow with do notation
$result = doo(
    let('a', Identity::of(1)),
    let('b', Identity::of(3)),
    let('c', in(['a', 'b'], function (int $a, int $b): Identity {
        return Identity::of($a + $b);
    })),
    in(['c'], function (int $c): Identity {
        return Identity::of($c * $c);
    })
);

assert($result === Identity::of(16));

Everyone needs to judge by itself, but in my opinion do notationimprove readability of code in PHP.

Collection constructor poses issue when trying to encapsulate an array

The current constructor of the Collection monad poses some issues when trying to encapsulate an array. If I want to store an empty array inside the collection, I have to do Collection::of([[]]) which is not isomorphic with say Maybe where I just do Just::of([]).

This is problematic for my implementation of filterM (see #26) where we try to construct the containing monad dynamically but we need a "special case" for Collection as it don't work right now.

One option could be to change the constructor of Collection to use func_get_args and never assume that a traversable passer as parameter should be stored as is.

What do you think ?

PS: I hope my description of the issue is clear.

mcompose failure

I am trying to use the mcompose function in the library and failing. I keep getting the following error in my console:

PHP Fatal error:  Uncaught Error: Call to undefined function Widmogrod\Functional\mcompose()

Maybe implementation, magic `null` value

Wouldn't it be better if Maybe was implemented the same way as Either:

With a generic Maybe interface and 2 specialisations Just and Nothing.

That way we could deal with the actual null value as a real value (which it is).

Thoughts?

Listt not found, PHP 7.0.14

Hey, I've added to my composer.json

...
"widmogrod/php-functional": "*"
...

but i get

Class 'Widmogrod\Primitive\Listt' not found

when i try to run

<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Core\Configure;
use Widmogrod\Functional as f;
use Widmogrod\Primitive\Listt;

class DebugController extends AppController
{
    public function initialize()
    {
        parent::initialize();
        $this->Auth->allow(['functional']);
    }

    public function functional()
    {
        $list = Listt::of([
            ['id' => 1, 'name' => 'One'],
            ['id' => 2, 'name' => 'Two'],
            ['id' => 3, 'name' => 'Three'],
        ]);
        exit();
    }
}

liftM2 documentation seems not coherent with its implementation

Hi,

I was looking to the documentation of liftM2:

* liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c

which is similar to the haskell version.

However the implementation seems to be wrong: $transformation is not used as a pure function.

function liftM2(
callable $transformation = null,
Monad $ma = null,
Monad $mb = null
) {
return curryN(
3,
function (
callable $transformation,
Monad $ma,
Monad $mb
) {
return $ma->bind(function ($a) use ($mb, $transformation) {
return $mb->bind(function ($b) use ($a, $transformation) {
return $transformation($a, $b);
});
});
}
)(...func_get_args());
}

It seems to be like bind2.
Am I missing something?

Thanks

Document design decisions

Of top of my head

  • functions should have docblock with Haskell type notation
  • Listt instead of PHP native [], Iterators, Generators
  • array as tuple
  • const for every function

constt return same closure when called without arguments

Hi,

I don't know if it's correct, but let's see this example:

$result = f\constt('foo')(); // returns the closure
$result = f\constt('foo')('bar); // returns "foo"

Is it correct? I think that it should return "foo" in both cases, evaluating it even when there are no parameters

Listtt - first class citizen

The Problem:
PHP has native array ([]) and \Traversable types like Iterator, Generator,...
To be able handle those types in php-functional a lot of nasty work need to be done to work on those types and do conversion between them:

This complicates learning path and implementation of many structures in this project.

A proposed solution:
My proposition to this problem is to have ONE type Listt.

  • every function, monad,... would know how to work on this representation
  • set of functions would be created to do adapt any Traversable type to this structure

TODO

  • standardise api
  • add method head() to Listt
  • add method tail()to Listt
  • add functions to adapt native PHP types to Listt
  • use Listt type every ware in project

Draft:

use Widmogrod\Functional as f;

$list = f\fromNil();
$list = f\prepend(1, $list); // x : xs
$list = f\append($listA, $listB); // xs ++ xs
$list = f\map(add(1), $list);
$head = f\head($list);
$tail = f\tail($list);
use Widmogrod\Functional as f;

$list = f\fromIterable(generate(100));
$list = f\fromIterable([1,2,3]);
$list = f\fromValue(1) // [1]

Explicit minimum PHP version

In #31, @widmogrod said that the minimum PHP version supported is 5.6, referring to the Travis build configuration file.
It would be interesting to explicitly declare it in composer.json

Also note that the current tests are very close to passing on end-of-life PHP 5.5 (tested with PHP 5.5.9-1ubuntu4.20). Only Product and Sum primitives trigger an error due to their constructor, which is both inherited from their parent class Num and from the PointedTrait trait. This seems not to be a problem on more recent PHP versions

How about migrating the CI from Travis to Github Actions?

Hi,
lately I migrated the CI of some projects of mine from Travis to Github Actions.
The main reason was to escape from the new pricing model of Travis, which gaves to OSS projects a limited amount of free minutes of usage.
But I discovered also that Github Actions are quite faster than Travis.

Could you be interested in them?

traverse on Collection monad returns the wrong container.

When using traverse on the Collection monad, the result should be of the type of the applicative, not the initial monad (ie Collection).

A small test case in Haskell :

Prelude Data.Traversable> let test x = if even x then Just(x) else Nothing
Prelude Data.Traversable> traverse test [1, 2, 3, 4]
Nothing
Prelude Data.Traversable> traverse test [2, 4]
Just [2,4]
Prelude Data.Traversable> 

The same test with PHP :

[2] boris> require_once('vendor/autoload.php');
[3] boris> use function Monad\Maybe\just;
[4] boris> use function Monad\Maybe\nothing;;
[5] boris> use Monad\Collection;
[6] boris> function test($x) { return $x % 2 == 1 ? nothing() : just($x); }
[7] boris> (new Collection([1, 2, 3, 4]))->traverse('test');
// object(Monad\Maybe\Nothing)(
// 
// )
[8] boris> (new Collection([2, 4]))->traverse('test');
// object(Monad\Collection)(
// 
// )
[9] boris> (new Collection([2, 4]))->traverse('test')->extract();
// array(
//   0 => array(
//     0 => 2,
//     1 => 4
//   )
// )

As we can see, it works well when there is a Nothing involved, otherwise there's an issue.

One possibility would be to "extract" the right type by running the transformation callable on the first item before doing the foldr so we could feed the correct of function.

What do you think ?

Namespace clash with functional-php

Hello there,

Thanks for the great work on this library, it's nice to have some monad implementation especially based on FantasyLand :)

I have one not so small issue however. I usually like to use https://github.com/lstrojny/functional-php but sadly when installing both packages via composer there is a nameclash on the invoke that you both define in the Functional namespace.

PHP Fatal error:  Cannot redeclare Functional\invoke() (previously declared in /vagrant/src/vendor/lstrojny/functional-php/src/Functional/Invoke.php:37) in /vagrant/src/vendor/widmogrod/php-functional/src/Functional/functions.php on line 94

I am not sure what the best solution is. As fas as I know, it is not possible to fix this via composer. I would like to propose a renaming of your namespace as this library is probably less used than "functional-php" thus leading to fewer BC breaks.

I can see two possible renames :

  • rename just the Functional namespace to something else, like FunctionalHelpers for examples. But there might be issues for other namespaces like Monad later.
  • put everything "under" a parent namespace like Widmogrod or something like that.

What do you think ?

I can provide a PR doing all that as soon as we agree on the best solution.

Cheers,

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.