diff --git a/example/ListComprehensionWithMonadTest.php b/example/ListComprehensionWithMonadTest.php index 552d8cd..30b0842 100644 --- a/example/ListComprehensionWithMonadTest.php +++ b/example/ListComprehensionWithMonadTest.php @@ -20,14 +20,13 @@ public function test_it_should_combine_two_lists() }); }); - $this->assertEquals( - fromIterable([ - [1, 'a'], - [1, 'b'], - [2, 'a'], - [2, 'b'] - ]), - $result - ); + $expected = fromIterable([ + [1, 'a'], + [1, 'b'], + [2, 'a'], + [2, 'b'] + ]); + + $this->assertTrue($expected->equals($result)); } } diff --git a/src/Functional/functions.php b/src/Functional/functions.php index bed18bb..6978612 100644 --- a/src/Functional/functions.php +++ b/src/Functional/functions.php @@ -11,6 +11,7 @@ use Widmogrod\FantasyLand\Monad; use Widmogrod\FantasyLand\Traversable; use Widmogrod\Monad\Identity; +use Widmogrod\Primitive\EmptyListError; use Widmogrod\Primitive\Listt; use Widmogrod\Primitive\ListtCons; @@ -278,20 +279,33 @@ function foldr(callable $callable, $accumulator = null, Foldable $foldable = nul /** * filter :: (a -> Bool) -> [a] -> [a] * - * @param callable $predicate - * @param Foldable $list + * Because I want to make list filtering lazy, + * implementation of this method diverge from Haskell one * - * @return Foldable + * ```haskell + * filter pred (x:xs) + * | pred x = x : filter pred xs + * | otherwise = filter pred xs + * ``` + * + * @param callable $predicate + * @param Listt $xs + * @return Listt */ -function filter(callable $predicate, Foldable $list = null) +function filter(callable $predicate, Listt $xs = null) { - return curryN(2, function (callable $predicate, Foldable $list) { - return reduce(function (Listt $list, $x) use ($predicate) { - return $predicate($x) - ? new ListtCons(function () use ($list, $x) { - return [$x, $list]; - }) : $list; - }, fromNil(), $list); + return curryN(2, function (callable $predicate, Listt $xs): Listt { + return new ListtCons(function () use ($predicate, $xs) { + try { + return $predicate(head($xs)) + ? new ListtCons(function () use ($predicate, $xs) { + return [head($xs), filter($predicate, tail($xs))]; + }) + : filter($predicate, tail($xs)); + } catch (EmptyListError $e) { + return fromNil(); + } + }); })(...func_get_args()); } diff --git a/src/Functional/miscellaneous.php b/src/Functional/miscellaneous.php index 2da884f..cc5f881 100644 --- a/src/Functional/miscellaneous.php +++ b/src/Functional/miscellaneous.php @@ -4,6 +4,21 @@ namespace Widmogrod\Functional; +/** + * @var callable + */ +const noop = 'Widmogrod\Functional\noop'; + +/** + * noop :: _ -> _ + * + * @return null + */ +function noop() +{ + return null; +} + /** * @var callable */ diff --git a/src/Primitive/ListtCons.php b/src/Primitive/ListtCons.php index ed0df28..f6c2b70 100644 --- a/src/Primitive/ListtCons.php +++ b/src/Primitive/ListtCons.php @@ -7,6 +7,8 @@ use Widmogrod\Common; use Widmogrod\FantasyLand; use Widmogrod\Functional as f; +use function Widmogrod\Functional\constt; +use const Widmogrod\Functional\noop; class ListtCons implements Listt, \IteratorAggregate { @@ -39,7 +41,16 @@ public function getIterator() { $tail = $this; do { - [$head, $tail] = $tail->headTail(); + $result = $tail->lazyHeadTail(function ($x, Listt $xs): array { + return [$x, $xs]; + }, noop); + + if (!is_array($result)) { + return; + } + + [$head, $tail] = $result; + yield $head; } while ($tail instanceof self); } @@ -50,9 +61,9 @@ public function getIterator() public function map(callable $transformation): FantasyLand\Functor { return new self(function () use ($transformation) { - [$head, $tail] = $this->headTail(); - - return [$transformation($head), $tail->map($transformation)]; + return $this->lazyHeadTail(function ($x, Listt $xs) use ($transformation) { + return [$transformation($x), $xs->map($transformation)]; + }, constt(self::mempty())); }); } @@ -148,9 +159,9 @@ public function concat(FantasyLand\Semigroup $value): FantasyLand\Semigroup if ($value instanceof self) { return new self(function () use ($value) { - [$x, $xs] = $this->headTail(); - - return [$x, $xs->concat($value)]; + return $this->lazyHeadTail(function ($x, Listt $xs) use ($value) { + return [$x, $xs->concat($value)]; + }, constt(self::mempty())); }); } @@ -162,7 +173,7 @@ public function concat(FantasyLand\Semigroup $value): FantasyLand\Semigroup */ public function equals($other): bool { - return $other instanceof self + return $other instanceof Listt ? $this->extract() === $other->extract() : false; } @@ -172,9 +183,11 @@ public function equals($other): bool */ public function head() { - [$head] = $this->headTail(); - - return $head; + return $this->lazyHeadTail(function ($head) { + return $head; + }, function () { + throw new EmptyListError(__FUNCTION__); + }); } /** @@ -182,13 +195,25 @@ public function head() */ public function tail(): Listt { - [$head, $tail] = $this->headTail(); - - return $tail; + return $this->lazyHeadTail(function ($head, $tail) { + return $tail; + }, function () { + throw new EmptyListError(__FUNCTION__); + }); } - public function headTail(): array + private function lazyHeadTail(callable $headTail, callable $nil) { - return call_user_func($this->next); + $result = $this; + + do { + $result = call_user_func($result->next); + } while ($result instanceof self); + + if ($result instanceof ListtNil) { + return $nil(); + } + + return $headTail(...$result); } } diff --git a/test/Functional/FilterTest.php b/test/Functional/FilterTest.php index 1f21dcf..ac24f8e 100644 --- a/test/Functional/FilterTest.php +++ b/test/Functional/FilterTest.php @@ -4,9 +4,12 @@ namespace test\Functional; +use Widmogrod\Primitive\Listt; use function Widmogrod\Functional\filter; use function Widmogrod\Functional\fromIterable; use function Widmogrod\Functional\fromNil; +use function Widmogrod\Functional\repeat; +use function Widmogrod\Functional\take; class FilterTest extends \PHPUnit\Framework\TestCase { @@ -14,16 +17,20 @@ class FilterTest extends \PHPUnit\Framework\TestCase * @dataProvider provideData */ public function test_it_should_filter_with_maybe( - $list, - $expected + Listt $list, + Listt $expected ) { $filter = function (int $i): bool { return $i % 2 === 1; }; - $this->assertEquals( - $expected, - filter($filter, $list) + $result = filter($filter, $list); + $r = print_r($result->extract(), true); + $e = print_r($expected->extract(), true); + + $this->assertTrue( + $result->equals($expected), + "$e != $r" ); } @@ -42,6 +49,10 @@ public function provideData() '$list' => fromIterable(new \ArrayIterator([1, 2, 3, 4, 5])), '$expected' => fromIterable([1, 3, 5]), ], + 'filter everything' => [ + '$list' => take(300, repeat(2)), + '$expected' => fromNil(), + ], ]; } }