Use free monad to design - haskell like do notation
authorwidmogrod <widmogrod@gmail.com>
Mon, 1 Jan 2018 21:54:53 +0000 (22:54 +0100)
committerwidmogrod <widmogrod@gmail.com>
Mon, 1 Jan 2018 21:54:53 +0000 (22:54 +0100)
composer.json
example/FreeDooDSLTest.php [new file with mode: 0644]
src/Functional/functions.php
src/Monad/Control/Doo/Algebra/DooF.php [new file with mode: 0644]
src/Monad/Control/Doo/Algebra/In.php [new file with mode: 0644]
src/Monad/Control/Doo/Algebra/Let.php [new file with mode: 0644]
src/Monad/Control/Doo/Registry/CannotRedeclareVariableError.php [new file with mode: 0644]
src/Monad/Control/Doo/Registry/Registry.php [new file with mode: 0644]
src/Monad/Control/Doo/Registry/VariableNotDeclaredError.php [new file with mode: 0644]
src/Monad/Control/Doo/actions.php [new file with mode: 0644]
src/Monad/Control/Doo/interpretation.php [new file with mode: 0644]

index 2dcdede..8076579 100644 (file)
@@ -47,6 +47,8 @@
       "src/Monad/Reader/functions.php",
       "src/Monad/State/functions.php",
       "src/Monad/Writer/functions.php",
+      "src/Monad/Control/Doo/actions.php",
+      "src/Monad/Control/Doo/interpretation.php",
       "src/Useful/match.php"
     ]
   }
diff --git a/example/FreeDooDSLTest.php b/example/FreeDooDSLTest.php
new file mode 100644 (file)
index 0000000..8412f0b
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace example;
+
+use function Widmogrod\Monad\Control\Doo\doo;
+use function Widmogrod\Monad\Control\Doo\in;
+use function Widmogrod\Monad\Control\Doo\let;
+use Widmogrod\Monad\Identity;
+
+class FreeDooDSLTest extends \PHPUnit\Framework\TestCase
+{
+    public function test_it()
+    {
+        $result = doo(
+            let('a', Identity::of(1)),
+            let('b', Identity::of(2)),
+            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);
+            })
+        );
+
+        $this->assertEquals(Identity::of(9), $result);
+    }
+}
index bed18bb..cbab20a 100644 (file)
@@ -10,7 +10,6 @@ use Widmogrod\FantasyLand\Foldable;
 use Widmogrod\FantasyLand\Functor;
 use Widmogrod\FantasyLand\Monad;
 use Widmogrod\FantasyLand\Traversable;
-use Widmogrod\Monad\Identity;
 use Widmogrod\Primitive\Listt;
 use Widmogrod\Primitive\ListtCons;
 
@@ -457,37 +456,27 @@ const sequenceM = 'Widmogrod\Functional\sequenceM';
  *
  * a.k.a haskell >>
  *
- * Sequentially compose two actions, discarding any value produced by the first, like sequencing operators (such as the semicolon) in imperative languages.
+ * Sequentially compose two actions, discarding any value produced by the first,
+ * like sequencing operators (such as the semicolon) in imperative languages.
+ *
+ * This implementation allow to **compose more than just two monads**.
  *
  * @param Monad $a
  * @param Monad $b
  *
  * @return Monad
  */
-function sequenceM(Monad $a, Monad $b)
-{
-    return $a->bind(function () use ($b) {
-        return $b;
-    });
-}
-
-/**
- * @var callable
- */
-const sequence_ = 'Widmogrod\Functional\sequence_';
-
-/**
- * sequence_ :: Monad m => [m a] -> m ()
- *
- * @todo consider to do it like this: foldr (>>) (return ())
- *
- * @param Monad[] $monads
- *
- * @return Monad
- */
-function sequence_(Monad ...$monads)
+function sequenceM(Monad $a, Monad $b = null): Monad
 {
-    return reduce(sequenceM, Identity::of([]), fromIterable($monads));
+    return curryN(2, function (Monad ...$monads): Monad {
+        return array_reduce($monads, function (?Monad $a, Monad $b) {
+            return $a
+                ? $a->bind(function () use ($b) {
+                    return $b;
+                })
+                : $b;
+        }, null);
+    })(...func_get_args());
 }
 
 /**
@@ -516,23 +505,6 @@ function traverse(callable $transformation, Traversable $t = null)
 }
 
 /**
- * @var callable
- */
-const sequence = 'Widmogrod\Functional\sequence';
-
-/**
- * sequence :: Monad m => t (m a) -> m (t a)
- *
- * @param Traversable|Monad[] $monads
- *
- * @return Monad
- */
-function sequence(Monad ...$monads)
-{
-    return traverse(identity, fromIterable($monads));
-}
-
-/**
  * filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]
  *
  * ```haskell
diff --git a/src/Monad/Control/Doo/Algebra/DooF.php b/src/Monad/Control/Doo/Algebra/DooF.php
new file mode 100644 (file)
index 0000000..9702dfc
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+namespace Widmogrod\Monad\Control\Doo\Algebra;
+
+use Widmogrod\FantasyLand\Functor;
+use Widmogrod\Useful\PatternMatcher;
+
+/**
+ *  DooF next = Let name m next
+ *            | In [name] fn
+ */
+interface DooF extends Functor, PatternMatcher
+{
+}
diff --git a/src/Monad/Control/Doo/Algebra/In.php b/src/Monad/Control/Doo/Algebra/In.php
new file mode 100644 (file)
index 0000000..019b49a
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+namespace Widmogrod\Monad\Control\Doo\Algebra;
+
+use Widmogrod\FantasyLand\Functor;
+
+class In implements DooF
+{
+    private $names;
+    private $fn;
+
+    public function __construct(array $names, callable $fn)
+    {
+        $this->names = $names;
+        $this->fn = $fn;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function map(callable $function): Functor
+    {
+        return new self(
+            $this->names,
+            $this->fn
+        );
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function patternMatched(callable $fn)
+    {
+        return $fn($this->names, $this->fn);
+    }
+}
diff --git a/src/Monad/Control/Doo/Algebra/Let.php b/src/Monad/Control/Doo/Algebra/Let.php
new file mode 100644 (file)
index 0000000..89640a3
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+namespace Widmogrod\Monad\Control\Doo\Algebra;
+
+use Widmogrod\FantasyLand\Functor;
+use Widmogrod\FantasyLand\Monad;
+use Widmogrod\Monad\Free\MonadFree;
+
+class Let implements DooF
+{
+    private $name;
+    private $m;
+    private $next;
+
+    public function __construct(string $name, Monad $m, MonadFree $next)
+    {
+        $this->name = $name;
+        $this->m = $m;
+        $this->next = $next;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function map(callable $function): Functor
+    {
+        return new self(
+            $this->name,
+            $this->m,
+            $function($this->next)
+        );
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function patternMatched(callable $fn)
+    {
+        return $fn($this->name, $this->m, $this->next);
+    }
+}
diff --git a/src/Monad/Control/Doo/Registry/CannotRedeclareVariableError.php b/src/Monad/Control/Doo/Registry/CannotRedeclareVariableError.php
new file mode 100644 (file)
index 0000000..3b10b79
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+namespace Widmogrod\Monad\Control\Doo\Registry;
+
+class CannotRedeclareVariableError extends \Exception
+{
+    public function __construct(string $name, array $registered)
+    {
+        $message = 'Cannot redeclare variable "%s". Registered variables %s';
+        $message = sprintf($message, $name, join(',', $registered));
+        parent::__construct($message);
+    }
+}
diff --git a/src/Monad/Control/Doo/Registry/Registry.php b/src/Monad/Control/Doo/Registry/Registry.php
new file mode 100644 (file)
index 0000000..02c91aa
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+namespace Widmogrod\Monad\Control\Doo\Registry;
+
+class Registry
+{
+    private $data = [];
+
+    /**
+     * @param  string                   $name
+     * @return mixed
+     * @throws VariableNotDeclaredError
+     */
+    public function get(string $name)
+    {
+        if (array_key_exists($name, $this->data)) {
+            return $this->data[$name];
+        }
+
+        throw new VariableNotDeclaredError($name);
+    }
+
+    /**
+     * @param  string                       $name
+     * @param  mixed                        $value
+     * @return mixed
+     * @throws CannotRedeclareVariableError
+     */
+    public function set(string $name, $value)
+    {
+        if (array_key_exists($name, $this->data)) {
+            throw new CannotRedeclareVariableError($name, array_keys($this->data));
+        }
+
+        return $this->data[$name] = $value;
+    }
+}
diff --git a/src/Monad/Control/Doo/Registry/VariableNotDeclaredError.php b/src/Monad/Control/Doo/Registry/VariableNotDeclaredError.php
new file mode 100644 (file)
index 0000000..d43134d
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+namespace Widmogrod\Monad\Control\Doo\Registry;
+
+class VariableNotDeclaredError extends \Exception
+{
+    public function __construct(string $name)
+    {
+        $message = 'Variable "%s" is not declared';
+        $message = sprintf($message, $name);
+
+        parent::__construct($message);
+    }
+}
diff --git a/src/Monad/Control/Doo/actions.php b/src/Monad/Control/Doo/actions.php
new file mode 100644 (file)
index 0000000..dcd0bb2
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+namespace Widmogrod\Monad\Control\Doo;
+
+use Widmogrod\FantasyLand\Monad;
+use Widmogrod\Monad\Free\MonadFree;
+use Widmogrod\Monad\Free\Pure;
+use function Widmogrod\Monad\Free\liftF;
+
+function let(string $name, Monad $m): MonadFree
+{
+    return $m instanceof MonadFree
+        ? $m->bind(function (Monad $m) use ($name): MonadFree {
+            return liftF(new Algebra\Let($name, $m, Pure::of(null)));
+        })
+        : liftF(new Algebra\Let($name, $m, Pure::of(null)));
+}
+
+function in(array $names, callable $fn): MonadFree
+{
+    return liftF(new Algebra\In($names, $fn));
+}
diff --git a/src/Monad/Control/Doo/interpretation.php b/src/Monad/Control/Doo/interpretation.php
new file mode 100644 (file)
index 0000000..41bfee1
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+namespace Widmogrod\Monad\Control\Doo;
+
+use Widmogrod\FantasyLand\Monad;
+use Widmogrod\Monad\Control\Doo\Algebra\DooF;
+use Widmogrod\Monad\Control\Doo\Algebra\In;
+use Widmogrod\Monad\Control\Doo\Algebra\Let;
+use Widmogrod\Monad\Control\Doo\Registry\Registry;
+use Widmogrod\Monad\Free\MonadFree;
+use Widmogrod\Monad\Free\Pure;
+use Widmogrod\Monad\Reader;
+use const Widmogrod\Monad\Reader\pure;
+use function Widmogrod\Functional\sequenceM;
+use function Widmogrod\Monad\Free\foldFree;
+use function Widmogrod\Monad\Reader\runReader;
+use function Widmogrod\Useful\match;
+
+/**
+ * @var callable
+ */
+const interpretation = 'Widmogrod\Monad\Control\Doo\interpretation';
+
+/**
+ * interpretationOfDoo :: DooF f -> Reader Registry MonadFree
+ *
+ * @param  DooF   $f
+ * @return Reader
+ *
+ * @throws \Widmogrod\Useful\PatternNotMatchedError
+ */
+function interpretation(DooF $f)
+{
+    return match([
+        Let::class => function (string $name, Monad $m, MonadFree $next): Reader {
+            return Reader::of(function (Registry $registry) use ($name, $m, $next) {
+                return $m->bind(function ($v) use ($name, $next, &$registry) {
+                    $registry->set($name, $v);
+
+                    return $next;
+                });
+            });
+        },
+        In::class => function (array $names, callable $fn): Reader {
+            return Reader::of(function (Registry $registry) use ($names, $fn) {
+                $args = array_map(function ($name) use ($registry) {
+                    return $registry->get($name);
+                }, $names);
+
+                return Pure::of($fn(...$args));
+            });
+        },
+    ], $f);
+}
+
+function doo(MonadFree ...$operation)
+{
+    $registry = new Registry();
+
+    return runReader(
+        foldFree(interpretation, sequenceM(...$operation), pure),
+        $registry
+    );
+}