Repository: ircmaxell/monad-php Branch: master Commit: c9a4970eb36d Files: 15 Total size: 15.2 KB Directory structure: gitextract_hl151y4g/ ├── .gitignore ├── README.md ├── composer.json ├── lib/ │ └── MonadPHP/ │ ├── Chain.php │ ├── Deferred.php │ ├── Identity.php │ ├── ListMonad.php │ ├── Maybe.php │ ├── Monad.php │ └── Promise.php ├── phpunit.xml.dist └── test/ └── MonadPHP/ ├── IdentityTest.php ├── ListMonadTest.php ├── MaybeTest.php └── PromiseTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ vendor ================================================ FILE: README.md ================================================ MonadPHP ======== This is a basic Monad library for PHP. Usage ===== Values are "wrapped" in the monad via either the constructor: `new MonadPHP\Identity($value)` or the `unit()` method on an existing instance: `$monad->unit($value);` Functions can be called on the wrapped value using `bind()`: use MonadPHP\Identity; $monad = new Identity(1); $monad->bind(function($value) { var_dump($value); }); // Prints int(1) All calls to bind return a new monad instance wrapping the return value of the function. use MonadPHP\Identity; $monad = new Identity(1); $monad->bind(function($value) { return 2 * $value; })->bind(function($value) { var_dump($value); }); // Prints int(2) Additionally, "extracting" the raw value is supported as well (since this is PHP and not a pure functional language)... use MonadPHP\Identity; $monad = new Identity(1); var_dump($monad->extract()); // Prints int(1) Maybe Monad =========== One of the first useful monads, is the Maybe monad. The value here is that it will only call the callback provided to `bind()` if the value it wraps is not `null`. use MonadPHP\Maybe; $monad = new Maybe(1); $monad->bind(function($value) { var_dump($value); }); // prints int(1) $monad = new Maybe(null); $monad->bind(function($value) { var_dump($value); }); // prints nothing (callback never called)... The included Chain monad does the same thing, but providing a short-cut implementation for objects: use MonadPHP\Chain; $monad = new Chain($someChainableObject); $obj = $monad->call1()->call2()->nonExistantMethod()->call4()->extract(); var_dump($obj); // null This can prevent errors when used with chaining... List Monad ========== This abstracts away the concept of a list of items (an array): use MonadPHP\ListMonad; $monad = new ListMonad(array(1, 2, 3, 4)); $doubled = $monad->bind(function($value) { return 2 * $value; }); var_dump($doubled->extract()); // Prints array(2, 4, 6, 8) Note that the passed in function gets called once per value, so it only ever deals with a single element, never the entire array... It also works with any `Traversable` object (like iterators, etc). Just be aware that returning the new monad that's wrapped will alwyas become an array... Composition =========== These Monads can be composed together to do some really useful things: use MonadPHP\ListMonad; use MonadPHP\Maybe; $monad = new ListMonad(array(1, 2, 3, null, 4)); $newMonad = $monad->bind(function($value) { return new Maybe($value); }); $doubled = $newMonad->bind(function($value) { return 2 * $value; }); var_dump($doubled->extract()); // Prints array(2, 4, 6, null, 8) Or, what if you want to deal with multi-dimensional arrays? use MonadPHP\ListMonad; $monad = new ListMonad(array(array(1, 2), array(3, 4), array(5, 6))); $newMonad = $monad->bind(function($value) { return new ListMonad($value); }); $doubled = $newMonad->bind(function($value) { return 2 * $value; }); var_dump($doubled->extract()); // Prints array(array(2, 4), array(6, 8), array(10, 12)) There also exist helper constants on each of the monads to get a callback to the `unit` method: $newMonad = $monad->bind(Maybe::unit); // Does the same thing as above Real World Example ================== Imagine that you want to traverse a multi-dimensional array to create a list of values of a particular sub-key. For example: $posts = array( array("title" => "foo", "author" => array("name" => "Bob", "email" => "bob@example.com")), array("title" => "bar", "author" => array("name" => "Tom", "email" => "tom@example.com")), array("title" => "baz"), array("title" => "biz", "author" => array("name" => "Mark", "email" => "mark@example.com")), ); What if we wanted to extract all author names from this data set. In traditional procedural programming, you'd likely have a number of loops and conditionals. With monads, it becomes quite simple. First, we define a function to return a particular index of an array: function index($key) { return function($array) use ($key) { return isset($array[$key]) ? $array[$key] : null; }; } Basically, this just creates a callback which will return a particular array key if it exists. With this, we have everything we need to get the list of authors. $postMonad = new MonadPHP\ListMonad($posts); $names = $postMonad ->bind(MonadPHP\Maybe::unit) ->bind(index("author")) ->bind(index("name")) ->extract(); Follow through and see what happens! ================================================ FILE: composer.json ================================================ { "require": { "php": ">=5.3.0" }, "autoload": { "psr-0": { "MonadPHP": "lib" } } } ================================================ FILE: lib/MonadPHP/Chain.php ================================================ bind(function($obj) use($name, $value) { $obj->$name = $value; return $value; }); } public function __get($name) { return $this->bind(function($obj) use($name) { if (isset($obj->$name)) { return $obj->$name; } return null; }); } public function __call($name, array $args = array()) { return $this->bind(function($obj) use ($name, $args) { if (is_callable(array($obj, $name))) { return call_user_func_array(array($obj, $name), $args); } return null; }); } } ================================================ FILE: lib/MonadPHP/Deferred.php ================================================ resolve(true, $value); } public function fail($value) { $this->resolve(false, $value); } } ================================================ FILE: lib/MonadPHP/Identity.php ================================================ value as $value) { $result[] = $this->runCallback($function, $value, $args); } return $this::unit($result); } public function extract() { $ret = array(); foreach ($this->value as $value) { if ($value instanceof Monad) { $ret[] = $value->extract(); } else { $ret[] = $value; } } return $ret; } } ================================================ FILE: lib/MonadPHP/Maybe.php ================================================ value)) { return parent::bind($function); } return $this::unit(null); } } ================================================ FILE: lib/MonadPHP/Monad.php ================================================ value = $value; } public static function unit($value) { if ($value instanceof static) { return $value; } return new static($value); } public function bind($function, array $args = array()) { return $this::unit($this->runCallback($function, $this->value, $args)); } public function extract() { if ($this->value instanceof self) { return $this->value->extract(); } return $this->value; } protected function runCallback($function, $value, array $args = array()) { if ($value instanceof self) { return $value->bind($function, $args); } array_unshift($args, $value); return call_user_func_array($function, $args); } } ================================================ FILE: lib/MonadPHP/Promise.php ================================================ success = $success; $this->failure = $failure; } public static function unit($success = null, $failure = null) { return new Promise($success, $failure); } public function bind($success, $failure) { $obj = $this->unit($success, $failure); if ($this->isResolved) { $obj->resolve($this->succeed, $this->value); } else { $this->children[] = $obj; } return $obj; } protected function resolve($status, $value) { if ($this->isResolved) { throw new \BadMethodCallException('Promise already resolved'); } $this->value = $value; $this->isResolved = true; $this->succeed = $status; $callback = $status ? $this->success : $this->failure; if ($callback) { $this->value = call_user_func($callback, $value); } foreach ($this->children as $child) { $child->resolve($status, $this->value); } } public function when($success = null, $failure = null) { return $this->bind($success, $failure); } } ================================================ FILE: phpunit.xml.dist ================================================ ./test/ ./lib/ ================================================ FILE: test/MonadPHP/IdentityTest.php ================================================ assertEquals($monad->unit(1), $monad->bind('intval')); } public function testBindUnit() { $monad = new Identity(1); $this->assertEquals($monad, $monad->bind($monad::unit)); } public function testExtract() { $monad = new Identity(new Maybe(1)); $this->assertEquals(1, $monad->extract()); } } ================================================ FILE: test/MonadPHP/ListMonadTest.php ================================================ setExpectedException('InvalidArgumentException'); $monad = new ListMonad(123); } public function testBindEmpty() { $monad = new ListMonad(array()); $called = 0; $monad->bind(function($value) use (&$called) { $called++; return $value; }); $this->assertEquals(0, $called); } public function testBindNotEmpty() { $monad = new ListMonad(array(1, 2, 3)); $called = 0; $new = $monad->bind(function($value) use (&$called) { $called++; return $value; }); $this->assertEquals(3, $called); $this->assertEquals($monad, $new); $this->assertEquals(array(1, 2, 3), $new->extract()); } public function testComposedListMonad() { $monad = new ListMonad(array(1, 2, 3, null, 4)); $newMonad = $monad->bind(function ($v) { return new Maybe($v); }); $doubled = $newMonad->bind(function ($v) { return $v * 2; }); $this->assertEquals(array(2, 4, 6, null, 8), $doubled->extract()); } } ================================================ FILE: test/MonadPHP/MaybeTest.php ================================================ assertEquals(new Maybe("1"), $monad->bind("strval")); $monad2 = $monad->unit(null); $called = false; $func = function($a) use (&$called) { $called = true; }; $this->assertEquals(new Maybe(null), $monad2->bind($func)); $this->assertFalse($called); } public function testUnit() { $monad = new Maybe(1); $this->assertEquals($monad, $monad->unit($monad)); } public function testExtract() { $monad = new Maybe(1); $this->assertEquals(1, $monad->extract()); } public function testSelfUnit() { $monad = new Maybe(1); $m2 = $monad->unit($monad); $m3 = $monad->unit($m2); $this->assertEquals($monad, $m3); } } ================================================ FILE: test/MonadPHP/PromiseTest.php ================================================ when(function() use (&$calledSuccess) { $calledSuccess = true; }, function() use (&$calledFailure) { $calledFailure = true; }); $this->assertFalse($calledSuccess); $this->assertFalse($calledFailure); $promise->fail(true); $this->assertFalse($calledSuccess, 'Not successful'); $this->assertTrue($calledFailure, 'Failed!'); try { $promise->succeed(true); $this->fail('No exception raised'); } catch (\BadMethodCallException $e) { $this->assertFalse($calledSuccess); } } public function testSucceed() { $promise = new Deferred; $calledSuccess = false; $calledFailure = false; $promise->when(function() use (&$calledSuccess) { $calledSuccess = true; }, function() use (&$calledFailure) { $calledFailure = true; }); $this->assertFalse($calledSuccess); $this->assertFalse($calledFailure); $promise->succeed(true); $this->assertTrue($calledSuccess, 'Success was not called'); $this->assertFalse($calledFailure, 'Fail was called'); try { $promise->fail(true); $this->fail('No exception raised'); } catch (\BadMethodCallException $e) { $this->assertFalse($calledFailure, 'Fail is true!'); } } public function testAlreadyResolvedPromise() { $promise = new Deferred(); $promise->succeed(true); $calledSuccess = false; $calledFailure = false; $promise->when(function() use (&$calledSuccess) { $calledSuccess = true; }, function() use (&$calledFailure) { $calledFailure = true; }); $this->assertTrue($calledSuccess); $this->assertFalse($calledFailure); } public function testPromiseValues() { $promise = new Deferred(); $receivedValue = null; $promise->when(function($a) { return $a + 1; })->when(function($b) use (&$receivedValue) { $receivedValue = $b; }); $this->assertEquals(null, $receivedValue); $promise->succeed(2); $this->assertEquals(3, $receivedValue); } }