From 88fff5027d4fb2d980860ec34bd076a4c6b47754 Mon Sep 17 00:00:00 2001 From: widmogrod Date: Wed, 20 Dec 2017 01:13:51 +0100 Subject: [PATCH] refactor filterM foldM --- example/WriterMonadTest.php | 4 +- src/Functional/functions.php | 157 ++++++++++++---------------------------- src/Functional/listt.php | 2 +- test/Functional/FilterMTest.php | 21 +++--- test/Functional/FlipTest.php | 39 ++++++---- test/Functional/FoldMTest.php | 26 ++++--- test/Functional/HeadTest.php | 43 ----------- test/Functional/TailTest.php | 35 --------- 8 files changed, 101 insertions(+), 226 deletions(-) delete mode 100644 test/Functional/HeadTest.php delete mode 100644 test/Functional/TailTest.php diff --git a/example/WriterMonadTest.php b/example/WriterMonadTest.php index 7b0047e..0a50e4f 100644 --- a/example/WriterMonadTest.php +++ b/example/WriterMonadTest.php @@ -10,7 +10,7 @@ class WriterMonadTest extends \PHPUnit_Framework_TestCase { public function test_it_should_filter_with_logs() { - $data = [1, 10, 15, 20, 25]; + $data = f\fromIterable([1, 10, 15, 20, 25]); $filter = function ($i) { if ($i % 2 == 1) { @@ -25,7 +25,7 @@ class WriterMonadTest extends \PHPUnit_Framework_TestCase list($result, $log) = f\filterM($filter, $data)->runWriter(); $this->assertEquals( - [10], + f\fromIterable([10]), $result ); $this->assertEquals( diff --git a/src/Functional/functions.php b/src/Functional/functions.php index 47bb13b..907d1aa 100644 --- a/src/Functional/functions.php +++ b/src/Functional/functions.php @@ -416,72 +416,6 @@ function toNativeTraversable($value) } /** - * @var callable - */ -const head = 'Widmogrod\Functional\head'; - -/** - * Return head of a traversable - * - * @deprecated Operation on native arrays will be replaced by Listt - * - * @param array|\Traversable $list - * - * @return null|mixed - */ -function head($list) -{ - if (!isNativeTraversable($list)) { - return null; - } - foreach ($list as $item) { - return $item; - } - - return null; -} - -/** - * @var callable - */ -const tail = 'Widmogrod\Functional\tail'; - -/** - * Return tail of a traversable - * - * @deprecated Operation on native arrays will be replaced by Listt - * - * @param array|\Traversable $list - * - * @return null|array - */ -function tail($list) -{ - if (!isNativeTraversable($list) || count($list) === 0) { - return null; - } - - if (is_array($list)) { - $clone = $list; - array_shift($clone); - - return $clone; - } - - $values = []; - $first = true; - foreach ($list as $k => $v) { - if ($first) { - $first = false; - } else { - $values[$k] = $v; - } - } - - return $values; -} - -/** * tryCatch :: Exception e => (a -> b) -> (e -> b) -> a -> b * * @deprecated Operation on native arrays will be replaced by Listt @@ -681,76 +615,79 @@ function sequence(Monad ...$monads) /** * filterM :: Monad m => (a -> m Bool) -> [a] -> m [a] * + * ```haskell + * filterM p = foldr (\ x -> liftA2 (\ flg -> if flg then (x:) else id) (p x)) (pure []) + * foldr :: (a -> b -> b) -> b -> t a -> b + * liftA2 :: (a -> b -> c) -> f a -> f b -> f c + *``` * @param callable $f (a -> m Bool) - * @param array|Traversable $collection [a] + * @param Foldable $xs [a] * * @return Monad m [a] */ -function filterM(callable $f, $collection) +function filterM(callable $f, Foldable $xs = null) { return curryN(2, function ( callable $f, - $collection + $xs ) { - /** @var Monad $monad */ - $monad = $f(head($collection)); - - $_filterM = function ($collection) use ($monad, $f, &$_filterM) { - if (count($collection) == 0) { - return $monad::of([]); + $result = foldr(function ($x, $ys) use ($f) { + $y = $f($x); + // Detect type of monad + if ($ys === null) { + $ys = $y::of(Listt::mempty()); } - $x = head($collection); - $xs = tail($collection); - - return $f($x)->bind(function ($bool) use ($x, $xs, $monad, $_filterM) { - return $_filterM($xs)->bind(function (array $acc) use ($bool, $x, $monad) { - if ($bool) { - array_unshift($acc, $x); - } + return liftA2(function (bool $flg, $ys) use ($x) { + return $flg + ? prepend($x, $ys) + : $ys; + }, $y, $ys); + }, null, $xs); - return $monad::of($acc); - }); - }); - }; - - return $_filterM($collection); + return $result === null + ? Listt::mempty() + : $result; })(...func_get_args()); } /** - * foldM :: Monad m => (a -> b -> m a) -> a -> [b] -> m a + * foldM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b + * + * ```haskell + * foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b + * foldlM f z0 xs = foldr f' return xs z0 + * where f' x k z = f z x >>= k + * + * foldr :: (a -> b -> b) -> b -> t a -> b + * ``` * * @param callable $f (a -> b -> m a) - * @param mixed $initial a - * @param array|\Traversable $collection [b] + * @param null $z0 + * @param Foldable $xs [b] * * @return mixed m a */ -function foldM(callable $f, $initial, $collection) +function foldM(callable $f, $z0 = null, Foldable $xs = null) { return curryN(3, function ( callable $f, - $initial, - $collection + $z0, + $xs ) { - /** @var Monad $monad */ - $monad = $f($initial, head($collection)); - - $_foldM = function ($acc, $collection) use ($monad, $f, &$_foldM) { - if (count($collection) == 0) { - return $monad::of($acc); + $result = foldr(function ($x, $k) use ($f, $z0) { + if ($k === null) { + return $f($z0, $x); + } else { + return $k->bind(function ($z) use ($f, $x) { + return $f($z, $x); + }); } + }, null, $xs); - $x = head($collection); - $xs = tail($collection); - - return $f($acc, $x)->bind(function ($result) use ($acc, $xs, $_foldM) { - return $_foldM($result, $xs); - }); - }; - - return $_foldM($initial, $collection); + return $result === null + ? Listt::mempty() + : $result; })(...func_get_args()); } diff --git a/src/Functional/listt.php b/src/Functional/listt.php index 0b83051..89bfb94 100644 --- a/src/Functional/listt.php +++ b/src/Functional/listt.php @@ -9,7 +9,7 @@ const fromIterable = 'Widmogrod\Functional\fromIterable'; function fromIterable(iterable $i): Listt { - return Listt::of(array_map(identity, $i)); + return Listt::of($i); } const fromValue = 'Widmogrod\Functional\fromValue'; diff --git a/test/Functional/FilterMTest.php b/test/Functional/FilterMTest.php index 6ecb2fd..2795021 100644 --- a/test/Functional/FilterMTest.php +++ b/test/Functional/FilterMTest.php @@ -2,8 +2,9 @@ namespace test\Functional; -use Widmogrod\Functional as f; -use Widmogrod\Monad\Maybe\Just; +use function Widmogrod\Functional\filterM; +use function Widmogrod\Functional\fromIterable; +use function Widmogrod\Monad\Maybe\just; class FilterMTest extends \PHPUnit_Framework_TestCase { @@ -15,12 +16,12 @@ class FilterMTest extends \PHPUnit_Framework_TestCase $expected ) { $filter = function ($i) { - return new Just($i % 2 == 1); + return just($i % 2 == 1); }; $this->assertEquals( $expected, - f\filterM($filter, $list)->extract() + filterM($filter, $list) ); } @@ -28,16 +29,16 @@ class FilterMTest extends \PHPUnit_Framework_TestCase { return [ 'simple list' => [ - '$list' => [1, 2, 3, 4, 5], - '$expected' => [1, 3, 5] + '$list' => fromIterable([1, 2, 3, 4, 5]), + '$expected' => just(fromIterable([1, 3, 5])) ], 'empty list' => [ - '$list' => [], - '$expected' => [] + '$list' => fromIterable([]), + '$expected' => fromIterable([]) ], 'traversable' => [ - '$list' => new \ArrayIterator([1, 2, 3, 4, 5]), - '$expected' => [1, 3, 5] + '$list' => fromIterable(new \ArrayIterator([1, 2, 3, 4, 5])), + '$expected' => just(fromIterable([1, 3, 5])), ], ]; } diff --git a/test/Functional/FlipTest.php b/test/Functional/FlipTest.php index 8097070..0d1472c 100644 --- a/test/Functional/FlipTest.php +++ b/test/Functional/FlipTest.php @@ -29,36 +29,49 @@ class FlipTest extends \PHPUnit_Framework_TestCase '$func' => function ($a, $b) { return [$a, $b]; }, - '$args' => [1, 2], + '$args' => [1, 2], '$expected' => [2, 1] ], 'three arguments' => [ '$func' => function ($a, $b, $c) { return [$a, $b, $c]; }, - '$args' => [1, 2, 3], + '$args' => [1, 2, 3], '$expected' => [2, 1, 3] ], ]; } /** - * @dataProvider provideFunctions + * @dataProvider provideFunctionsWithNotEnoughArgs */ public function test_it_should_curry_if_not_enough_args_passed( callable $func, - array $args, - $expected + array $args ) { - $flipped = f\flip($func); - $x = f\head($args); - $xs = f\tail($args); - - $curried = $flipped($x); + $curried = f\curry($func); - $this->assertEquals( - $expected, - call_user_func_array($curried, $xs) + $this->assertInstanceOf( + \Closure::class, + call_user_func_array($curried, $args) ); } + + public function provideFunctionsWithNotEnoughArgs() + { + return [ + 'two arguments' => [ + '$func' => function ($a, $b) { + return [$a, $b]; + }, + '$args' => [], + ], + 'three arguments' => [ + '$func' => function ($a, $b, $c) { + return [$a, $b, $c]; + }, + '$args' => [1], + ], + ]; + } } diff --git a/test/Functional/FoldMTest.php b/test/Functional/FoldMTest.php index e605bd2..70032a8 100644 --- a/test/Functional/FoldMTest.php +++ b/test/Functional/FoldMTest.php @@ -2,8 +2,10 @@ namespace test\Functional; -use Widmogrod\Functional as f; -use Widmogrod\Monad\Maybe as m; +use function Widmogrod\Functional\foldM; +use function Widmogrod\Functional\fromIterable; +use function Widmogrod\Monad\Maybe\just; +use function Widmogrod\Monad\Maybe\nothing; class FoldMTest extends \PHPUnit_Framework_TestCase { @@ -15,11 +17,11 @@ class FoldMTest extends \PHPUnit_Framework_TestCase $expected ) { $addSingleDigit = function ($acc, $i) { - return $i > 9 ? m\nothing() : m\just($acc + $i); + return $i > 9 ? nothing() : just($acc + $i); }; $this->assertEquals( $expected, - f\foldM($addSingleDigit, 0, $list)->extract() + foldM($addSingleDigit, 0, $list) ); } @@ -27,20 +29,20 @@ class FoldMTest extends \PHPUnit_Framework_TestCase { return [ 'just' => [ - '$list' => [1, 3, 5, 7], - '$expected' => 16 + '$list' => fromIterable([1, 3, 5, 7]), + '$expected' => just(16) ], 'nothing' => [ - '$list' => [1, 3, 42, 7], - '$expected' => null + '$list' => fromIterable([1, 3, 42, 7]), + '$expected' => nothing(), ], 'empty array' => [ - '$list' => [], - '$expected' => 0 + '$list' => fromIterable([]), + '$expected' => fromIterable([]), ], 'traversable' => [ - '$list' => new \ArrayIterator([1, 3, 5, 7]), - '$expected' => 16 + '$list' => fromIterable(new \ArrayIterator([1, 3, 5, 7])), + '$expected' => just(16) ], ]; } diff --git a/test/Functional/HeadTest.php b/test/Functional/HeadTest.php deleted file mode 100644 index 7c17d86..0000000 --- a/test/Functional/HeadTest.php +++ /dev/null @@ -1,43 +0,0 @@ -assertEquals( - $expected, - f\head($list) - ); - } - - public function provideData() - { - return [ - 'simple list' => [ - '$list' => [1, 2], - '$expected' => 1 - ], - 'empty list' => [ - '$list' => [], - '$expected' => null - ], - 'array iterator' => [ - '$list' => new \ArrayIterator([1, 2]), - '$expected' => 1 - ], - 'array object' => [ - '$list' => new \ArrayObject([1, 2]), - '$expected' => 1 - ], - ]; - } -} diff --git a/test/Functional/TailTest.php b/test/Functional/TailTest.php deleted file mode 100644 index 2633bd8..0000000 --- a/test/Functional/TailTest.php +++ /dev/null @@ -1,35 +0,0 @@ -assertEquals( - $expected, - f\tail($list) - ); - } - - public function provideData() - { - return [ - 'simple list' => [ - '$list' => [1, 2, 3], - '$expected' => [2, 3] - ], - 'empty list' => [ - '$list' => [], - '$expected' => null - ], - ]; - } -} -- 2.11.0