├── .gitignore ├── tests ├── bootstrap.php └── Bonami │ └── Collection │ ├── CallSpy.php │ ├── HelpersTest.php │ ├── EnumListTest.php │ ├── CurriedFunctionTest.php │ ├── Monoid │ └── MonoidTest.php │ ├── EnumTest.php │ ├── helpers.php │ ├── EitherTest.php │ ├── OptionTest.php │ ├── LazyListTest.php │ └── TrySafeTest.php ├── src └── Bonami │ └── Collection │ ├── Exception │ ├── CollectionException.php │ ├── InvalidStateException.php │ ├── NotImplementedException.php │ ├── ValueIsNotPresentException.php │ ├── OutOfBoundsException.php │ └── InvalidEnumValueException.php │ ├── Hash │ └── IHashable.php │ ├── Monoid │ ├── DoubleSumMonoid.php │ ├── IntSumMonoid.php │ ├── DoubleProductMonoid.php │ ├── IntProductMonoid.php │ ├── StringMonoid.php │ ├── Monoid.php │ └── OptionMonoid.php │ ├── EnumList.php │ ├── Mutable │ └── Map.php │ ├── Monad1.php │ ├── Monad2.php │ ├── helpers.php │ ├── Enum.php │ ├── Iterable1.php │ ├── Option.php │ ├── Either.php │ ├── TrySafe.php │ └── LazyList.php ├── phpstan.neon ├── Makefile ├── phpunit.xml ├── LICENSE ├── composer.json ├── CONTRIBUTING.md ├── docs ├── enum.md ├── curried-function.md ├── lift.md ├── type-classes.md ├── try-safe.md └── option.md ├── .github └── workflows │ └── main.yml ├── ruleset.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.phpunit* 3 | /bin 4 | /vendor 5 | composer.lock 6 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | > */ 12 | public function getCalls(): array; 13 | } 14 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | universalObjectCratesClasses: 4 | - stdClass 5 | - DOMElement 6 | - SimpleXMLElement 7 | inferPrivatePropertyTypeFromConstructor: true 8 | reportUnmatchedIgnoredErrors: false 9 | level: 8 10 | paths: 11 | - src/ 12 | - tests/ 13 | tmpDir: /tmp/phpstan 14 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Monoid/DoubleSumMonoid.php: -------------------------------------------------------------------------------- 1 | */ 8 | class DoubleSumMonoid implements Monoid 9 | { 10 | public function concat($a, $b) 11 | { 12 | return $a + $b; 13 | } 14 | 15 | public function getEmpty() 16 | { 17 | return 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Monoid/IntSumMonoid.php: -------------------------------------------------------------------------------- 1 | */ 8 | class IntSumMonoid implements Monoid 9 | { 10 | public function concat($a, $b): int 11 | { 12 | return $a + $b; 13 | } 14 | 15 | public function getEmpty(): int 16 | { 17 | return 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Monoid/DoubleProductMonoid.php: -------------------------------------------------------------------------------- 1 | */ 8 | class DoubleProductMonoid implements Monoid 9 | { 10 | public function concat($a, $b) 11 | { 12 | return $a * $b; 13 | } 14 | 15 | public function getEmpty() 16 | { 17 | return 1; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Monoid/IntProductMonoid.php: -------------------------------------------------------------------------------- 1 | */ 8 | class IntProductMonoid implements Monoid 9 | { 10 | public function concat($a, $b): int 11 | { 12 | return $a * $b; 13 | } 14 | 15 | public function getEmpty(): int 16 | { 17 | return 1; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Monoid/StringMonoid.php: -------------------------------------------------------------------------------- 1 | */ 8 | class StringMonoid implements Monoid 9 | { 10 | public function concat($a, $b): string 11 | { 12 | return $a . $b; 13 | } 14 | 15 | public function getEmpty(): string 16 | { 17 | return ''; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/HelpersTest.php: -------------------------------------------------------------------------------- 1 | $x * 3; 14 | $add2 = static fn (int $x): int => $x + 2; 15 | 16 | self::assertEquals(9, compose($multiplyBy3, $add2)(1)); 17 | self::assertEquals(5, compose($add2, $multiplyBy3)(1)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Bonami/Collection/EnumList.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class EnumList extends ArrayList 13 | { 14 | /** @return ArrayList */ 15 | public function getValueList(): ArrayList 16 | { 17 | return $this->map(static fn (Enum $enum) => $enum->getValue()); 18 | } 19 | 20 | /** @return array */ 21 | public function getValues(): array 22 | { 23 | return $this->getValueList()->toArray(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/EnumListTest.php: -------------------------------------------------------------------------------- 1 | $enumClass 18 | */ 19 | public function __construct($value, string $enumClass) 20 | { 21 | $valueType = gettype($value); 22 | $expectedValues = $enumClass::instanceList()->join(', '); 23 | 24 | $message = $valueType === 'object' 25 | ? sprintf('Invalid value "%s", one of scalar %s expected', $value::class, $expectedValues) 26 | : sprintf('Invalid %s value "%s", one of %s expected', $valueType, $value, $expectedValues); 27 | 28 | parent::__construct($message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Mutable/Map.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Map extends \Bonami\Collection\Map 16 | { 17 | /** 18 | * @param K $key 19 | * @param V $value 20 | */ 21 | public function add($key, $value): void 22 | { 23 | $keyHash = hashKey($key); 24 | $this->keys[$keyHash] = $key; 25 | $this->values[$keyHash] = $value; 26 | } 27 | 28 | /** 29 | * @param K $key 30 | * @param V $value 31 | * 32 | * @return V 33 | */ 34 | public function getOrAdd($key, $value) 35 | { 36 | $keyHash = hashKey($key); 37 | if (!array_key_exists($keyHash, $this->values)) { 38 | $this->keys[$keyHash] = $key; 39 | $this->values[$keyHash] = $value; 40 | } 41 | 42 | return $this->values[$keyHash]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | src 19 | 20 | 21 | 22 | 23 | tests 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Monoid/OptionMonoid.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | class OptionMonoid implements Monoid 17 | { 18 | /** @var Monoid */ 19 | private $monoid; 20 | 21 | /** @param Monoid $monoid */ 22 | public function __construct(Monoid $monoid) 23 | { 24 | $this->monoid = $monoid; 25 | } 26 | 27 | /** 28 | * Concats two options. If any of them is None, the result is None. 29 | * 30 | * @param Option $a 31 | * @param Option $b 32 | * 33 | * @return Option 34 | */ 35 | public function concat($a, $b): Option 36 | { 37 | return Option::lift(fn ($a, $b) => $this->monoid->concat($a, $b))($a, $b); 38 | } 39 | 40 | /** @return Option */ 41 | public function getEmpty(): Option 42 | { 43 | return Option::some($this->monoid->getEmpty()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Bonami.cz a.s. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/CurriedFunctionTest.php: -------------------------------------------------------------------------------- 1 | $a + $b); 14 | 15 | $plus5 = $curried(5); 16 | 17 | self::assertEquals(42, $plus5(37)); 18 | } 19 | 20 | public function testCurryN(): void 21 | { 22 | $curried = CurriedFunction::curry3(static fn (string $greeting, string $name, int $times): string => 23 | str_repeat(sprintf('%s %s,', $greeting, $name), $times)); 24 | 25 | self::assertEquals('Hello World,Hello World,', $curried('Hello')('World')(2)); 26 | } 27 | 28 | public function testMap(): void 29 | { 30 | $greeter = CurriedFunction::of(static fn(string $name): string => sprintf('Hello %s', $name)); 31 | $countChars = CurriedFunction::of(strlen(...)); 32 | 33 | self::assertEquals(11, CurriedFunction::of($greeter)->map($countChars)('World')); 34 | } 35 | 36 | public function testItShouldNotRewrapAlreadyWrapped(): void 37 | { 38 | $curried = CurriedFunction::curry2(static fn (int $a, int $b): int => $a + $b); 39 | 40 | $plus5 = $curried(5); 41 | self::assertSame($plus5, CurriedFunction::of($plus5)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bonami/collections", 3 | "type": "library", 4 | "description": "Collections library with focus on immutability and functional approach", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Jan Machala", 9 | "email": "jan.machala125@gmail.com" 10 | }, 11 | { 12 | "name": "Honza Trtik", 13 | "email": "honza.trtik@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=8.1", 18 | "ext-json": "*" 19 | }, 20 | "suggest": { 21 | "bonami/phpstan-collections": "Allow proper type resolving with phpstan" 22 | }, 23 | "require-dev": { 24 | "ergebnis/composer-normalize": "^2.0.2", 25 | "phpstan/phpstan": "~2.0.0", 26 | "phpunit/phpunit": "^9.4.2", 27 | "slevomat/coding-standard": "^8.15.0", 28 | "squizlabs/php_codesniffer": "^3.5.0" 29 | }, 30 | "config": { 31 | "bin-dir": "bin", 32 | "allow-plugins": { 33 | "dealerdirect/phpcodesniffer-composer-installer": true, 34 | "ergebnis/composer-normalize": true 35 | } 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "0.6.x-dev" 40 | } 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "": [ 45 | "src", 46 | "tests" 47 | ] 48 | }, 49 | "files": [ 50 | "src/Bonami/Collection/helpers.php" 51 | ] 52 | }, 53 | "autoload-dev": { 54 | "files": [ 55 | "tests/Bonami/Collection/helpers.php" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Monad1.php: -------------------------------------------------------------------------------- 1 | */ 11 | use Applicative1; 12 | 13 | /** 14 | * Default implementation of ap, derived from flatMap and map. It can be overridden by concrete 15 | * implementation 16 | * 17 | * @template A 18 | * @template B 19 | * 20 | * @param self> $closure 21 | * @param self $argument 22 | * 23 | * @return self 24 | */ 25 | final public static function ap(self $closure, self $argument): self 26 | { 27 | return $closure->flatMap(static fn (CurriedFunction $c) => $argument->map(static fn ($a) => $c($a))); 28 | } 29 | 30 | /** 31 | * Default implementation of product, derived from flatMap and map. It can be overridden by concrete 32 | * implemention 33 | * 34 | * @template A 35 | * @template B 36 | * 37 | * @param self $a 38 | * @param self $b 39 | * 40 | * @return self 41 | */ 42 | public static function product(self $a, self $b): self 43 | { 44 | return $a->flatMap(static fn ($x) => $b->map(static fn ($y) => [$x, $y])); 45 | } 46 | 47 | /** 48 | * Chain mapper call on Monad 49 | * 50 | * @template B 51 | * 52 | * @param callable(T, int): iterable $mapper 53 | * 54 | * @return self 55 | */ 56 | abstract public function flatMap(callable $mapper): self; 57 | } 58 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Monad2.php: -------------------------------------------------------------------------------- 1 | */ 14 | use Applicative2; 15 | 16 | /** 17 | * Default implementation of ap, derived from flatMap and map. It can be overridden by concrete 18 | * implementation 19 | * 20 | * @template A 21 | * @template B 22 | * 23 | * @param self> $closure 24 | * @param self $argument 25 | * 26 | * @return self 27 | */ 28 | final public static function ap(self $closure, self $argument): self 29 | { 30 | return $closure->flatMap(static fn ($c) => $argument->map(static fn ($a) => $c($a))); 31 | } 32 | 33 | /** 34 | * Default implementation of product, derived from flatMap and map. It can be overridden by concrete 35 | * implemention 36 | * 37 | * @template A 38 | * @template B 39 | * 40 | * @param self $a 41 | * @param self $b 42 | * 43 | * @return self 44 | */ 45 | public static function product(self $a, self $b): self 46 | { 47 | return $a->flatMap(static fn ($x) => $b->map(static fn ($y) => [$x, $y])); 48 | } 49 | 50 | /** 51 | * Chain mapper call on Monad 52 | * 53 | * @template B 54 | * 55 | * @param callable(R, int): iterable $mapper 56 | * 57 | * @return self 58 | */ 59 | abstract public function flatMap(callable $mapper): self; 60 | } 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Found a bug or something's broken? Easy raise an issue or even better fix it and send pull request. Any contributions are welcome. 4 | 5 | - Coding standard is PSR 1, PSR 2 and PSR 12. There are some other useful rules from Slevomat coding standards. They are enforced with phpcs. See `ruleset.xml` 6 | - The project aims to follow most [object calisthenics](https://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php) 7 | - Any contribution must provide tests for newly introduced conditions 8 | - Any un-confirmed issue needs a failing test case before being accepted 9 | - Pull requests must be sent from a new hotfix/feature branch, not from `master` 10 | - All GitHub workflow checks must pass before PR can be accepted 11 | 12 | ## Building 13 | 14 | You can run specific `make` or `composer` targets to help your development process. 15 | 16 | ### With Docker 17 | 18 | Initially you need to have Docker installed on your system. Afterwards run `make deps` in order to download dependencies. 19 | 20 | - `make deps` - downloads dependencies 21 | - `make test` - run tests and static-analysis 22 | - `make fmt-check` - check code style 23 | - `make fmt` - automatically fix your code style 24 | 25 | ### Without Docker 26 | 27 | You need to have PHP and Composer installed globally on your system. Afterwards run `composer update` in order to download dependencies. 28 | 29 | - `composer update` - downloads dependencies 30 | - `composer test` - run tests and static-analysis 31 | - `composer phpstan` - run static-analysis 32 | - `composer phpunit` - run tests 33 | - `composer phpcs` - check code style 34 | - `composer phpcfb` - automatically fix your code style 35 | 36 | -------------------------------------------------------------------------------- /src/Bonami/Collection/helpers.php: -------------------------------------------------------------------------------- 1 | $a <=> $b; 30 | } 31 | 32 | function descendingComparator(): callable 33 | { 34 | return static fn ($a, $b): int => $b <=> $a; 35 | } 36 | 37 | function tautology(): callable 38 | { 39 | return static fn (): bool => true; 40 | } 41 | 42 | function falsy(): callable 43 | { 44 | return static fn (): bool => false; 45 | } 46 | 47 | /** 48 | * Returns function that supplies $args as an arguments to passed function 49 | * 50 | * @template A 51 | * 52 | * @param A $arg $args 53 | * 54 | * @return callable(callable(A): mixed): mixed 55 | */ 56 | function applicator1($arg): callable 57 | { 58 | return static fn (callable $callable) => $callable($arg); 59 | } 60 | 61 | /** 62 | * @template A 63 | * @template B 64 | * @template C 65 | * 66 | * @param callable(B): C $f 67 | * @param callable(A): B $g 68 | * 69 | * @return callable(A): C 70 | */ 71 | function compose(callable $f, callable $g): callable 72 | { 73 | return static fn (...$args) => $f($g(...$args)); 74 | } 75 | 76 | function hashKey(mixed $key): int|string 77 | { 78 | if ($key === (object)$key) { 79 | return $key instanceof IHashable 80 | ? $key->hashCode() 81 | : spl_object_hash($key); 82 | } 83 | if (is_array($key)) { 84 | return serialize(array_map(static fn ($value) => hashKey($value), $key)); 85 | } 86 | if (is_null($key)) { 87 | return ""; 88 | } 89 | if (is_bool($key)) { 90 | return (int)$key; 91 | } 92 | 93 | return $key; 94 | } 95 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/Monoid/MonoidTest.php: -------------------------------------------------------------------------------- 1 | $monoid 20 | * @phpstan-param A $result 21 | */ 22 | public function testFromMonoids($a, $b, Monoid $monoid, $result): void 23 | { 24 | self::assertEquals($a, $monoid->concat($a, $monoid->getEmpty())); 25 | self::assertEquals($a, $monoid->concat($monoid->getEmpty(), $a)); 26 | self::assertEquals($result, $monoid->concat($a, $b)); 27 | } 28 | 29 | public function testDoubleSumMonoid(): void 30 | { 31 | $monoid = new DoubleSumMonoid(); 32 | 33 | self::assertEqualsWithDelta(1.1, $monoid->concat(1.1, $monoid->getEmpty()), 0.0001); 34 | self::assertEqualsWithDelta(1.1, $monoid->concat($monoid->getEmpty(), 1.1), 0.0001); 35 | self::assertEqualsWithDelta(3.2, $monoid->concat(1.1, 2.1), 0.0001); 36 | } 37 | 38 | public function testDoubleProductMonoid(): void 39 | { 40 | $monoid = new DoubleProductMonoid(); 41 | 42 | self::assertEqualsWithDelta(1.1, $monoid->concat(1.1, $monoid->getEmpty()), 0.0001); 43 | self::assertEqualsWithDelta(1.1, $monoid->concat($monoid->getEmpty(), 1.1), 0.0001); 44 | self::assertEqualsWithDelta(2.31, $monoid->concat(1.1, 2.1), 0.0001); 45 | } 46 | 47 | /** @phpstan-return iterable */ 48 | public function provideFixtures(): iterable 49 | { 50 | yield [1, 2, new IntSumMonoid(), 3]; 51 | yield [1, 2, new IntProductMonoid(), 2]; 52 | yield ['foo', 'bar', new StringMonoid(), 'foobar']; 53 | yield [Option::none(), Option::some(1), new OptionMonoid(new IntSumMonoid()), Option::none()]; 54 | yield [Option::some(1), Option::some(2), new OptionMonoid(new IntSumMonoid()), Option::some(3)]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/EnumTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidEnumValueException::class); 16 | $this->expectExceptionMessage('Invalid value "stdClass", one of scalar A, B, C expected'); 17 | 18 | TestEnum::create(new stdClass()); 19 | } 20 | 21 | public function testItShouldCreateInstanceByString(): void 22 | { 23 | self::assertEquals(TestEnum::create(TestEnum::A), TestEnum::create('A')); 24 | } 25 | 26 | public function testItShouldFailWhenValueOutOfDefinedValues(): void 27 | { 28 | $this->expectException(InvalidEnumValueException::class); 29 | $this->expectExceptionMessage('Invalid string value "D", one of A, B, C expected'); 30 | 31 | TestEnum::create('D'); 32 | } 33 | 34 | public function testItShouldGetNameOfConstantFromInstance(): void 35 | { 36 | self::assertEquals('A', TestEnum::create(TestEnum::A)->getConstName()); 37 | } 38 | 39 | public function testExists(): void 40 | { 41 | self::assertTrue(TestEnum::exists('A')); 42 | self::assertFalse(TestEnum::exists('D')); 43 | } 44 | 45 | public function testGetListComplement(): void 46 | { 47 | self::assertEquals( 48 | EnumList::fromIterable([TestEnum::create(TestEnum::B), TestEnum::create(TestEnum::C)]), 49 | TestEnum::getListComplement(TestEnum::create(TestEnum::A)), 50 | ); 51 | self::assertEquals( 52 | EnumList::fromIterable([TestEnum::create(TestEnum::B)]), 53 | TestEnum::getListComplement(TestEnum::create(TestEnum::A), TestEnum::create(TestEnum::C)), 54 | ); 55 | } 56 | 57 | public function testJsonSerializable(): void 58 | { 59 | self::assertJson( 60 | '{"foo": "A"}', 61 | json_encode(['foo' => TestEnum::create(TestEnum::A)], JSON_THROW_ON_ERROR), 62 | ); 63 | } 64 | } 65 | 66 | // @codingStandardsIgnoreStart 67 | class TestEnum extends Enum 68 | { 69 | public const A = 'A'; 70 | public const B = 'B'; 71 | public const C = 'C'; 72 | } 73 | // @codingStandardsIgnoreEnd 74 | -------------------------------------------------------------------------------- /docs/enum.md: -------------------------------------------------------------------------------- 1 | # Enum 2 | 3 | Enum is type with values from finite closed set (think of it as boolean type, which has exactly two possible values - true & false). 4 | 5 | ## Usage 6 | 7 | We can define our Enum extending abstract Enum class. All possible values are defined via constants. Constant values can be type of string or int 8 | ```php 9 | use Bonami\Collection\Enum; 10 | 11 | class Color extends Enum { 12 | 13 | const RED = "RED"; 14 | const BLUE = "BLUE"; 15 | const GREEN = "GREEN"; 16 | } 17 | ``` 18 | 19 | We can get instance of Enum by using `create` method. 20 | ```php 21 | $redColor = Color::create(Color::RED); 22 | ``` 23 | 24 | For more convinient instancing, we can add static constructors that will simplify Enum usage. 25 | ```php 26 | use Bonami\Collection\Enum; 27 | 28 | class Color extends Enum { 29 | 30 | const RED = "RED"; 31 | const BLUE = "BLUE"; 32 | const GREEN = "GREEN"; 33 | 34 | public static function RED(): Color { 35 | return self::create(self::RED); 36 | } 37 | 38 | public static function BLUE(): Color { 39 | return self::create(self::BLUE); 40 | } 41 | 42 | public static function GREEN(): Color { 43 | return self::create(self::GREEN); 44 | } 45 | } 46 | 47 | $redColor = Color::RED(); 48 | ``` 49 | 50 | All instances of same Enum value are equal. They share same reference. 51 | ```php 52 | // bool(true) 53 | var_dump(Color::RED() === Color::RED()); 54 | ``` 55 | 56 | ### Check existence of element 57 | ```php 58 | // bool(false) 59 | var_dump(Color::exists("BLACK")); 60 | 61 | // bool(true) 62 | var_dump(Color::exists("BLUE")); 63 | ``` 64 | 65 | ### List and Map support 66 | **TODO** doc links 67 | 68 | You can get all instances as List or Map. 69 | 70 | #### List 71 | ```php 72 | $lowercaseEnums = Color::instanceList() 73 | ->map(fn(Color $color): string => strtolower($color->getValue())) 74 | ->join(", "); 75 | 76 | // string(16) "red, blue, green" 77 | var_dump($lowercaseEnums); 78 | ``` 79 | 80 | #### Map 81 | ```php 82 | $colorReverseMap = Color::instanceMap() 83 | ->mapValues(fn(Color $color, string $key): string => strrev($color->getValue())) 84 | ->mapKeys(fn(string $color): string => strtolower($color)); 85 | 86 | // bool(true) 87 | var_dump($colorReverseMap->get('blue')->isDefined()); 88 | 89 | // string(4) "EULB" 90 | var_dump($colorReverseMap->get('blue')->getUnsafe()); 91 | ``` 92 | 93 | #### List Complement 94 | 95 | ```php 96 | $onlyRedColor = Color::getListComplement(Color::BLUE(), Color::GREEN()); 97 | 98 | // string(9) "RED" 99 | var_dump($onlyRedColor->join('')); 100 | ``` 101 | -------------------------------------------------------------------------------- /docs/curried-function.md: -------------------------------------------------------------------------------- 1 | # CurriedFunction 2 | 3 | Represents single argument function. Provides static factories to create curried versions of multi argument functions. 4 | 5 | ## Currying 6 | 7 | The concept is simple - it is transforming function taking multiple arguments into sequence of functions each taking single argument. 8 | 9 | ```php 10 | $greeter = fn (string $greeting, string $name): string => "{$greeting} {$name}!"; 11 | 12 | $curriedGreeter = CurriedFunction::curry2($greeter); 13 | 14 | $englishGreeter = $curriedGreeter("Hello"); // partial apply 15 | echo $englishGreeter("John"); // Hello John! 16 | echo $englishGreeter("Paul"); // Hello Paul! 17 | 18 | $spanishGreeter = $curriedGreeter("Hola"); // partial apply 19 | echo $spanishGreeter("Diego"); // Hola Diego! 20 | ``` 21 | 22 | You might wonder, where this can be useful. We can leverage this everywhere, where we expect single argument closure but we have two arguments function. 23 | 24 | E.g. 25 | 26 | ```php 27 | $strings = ArrayList::of('a', 'b'); 28 | $times1 = ArrayList::of(1, 2, 3); 29 | $times2 = ArrayList::of(4); 30 | 31 | $strRepeat = CurriedFunction::self::curry2(str_repeat(...)); 32 | 33 | $partiallyApplied = $strings->map($strRepeat); 34 | 35 | // ArrayList::fromIterable(['a', 'aa', 'aaa', 'b', 'bb', 'bbb'] 36 | $fullyApplied1 = $partiallyApplied->flatMap(fn ($pa) => $times1->map($pa)); 37 | 38 | // ArrayList::fromIterable(['aaaa', 'bbbb'] 39 | $fullyApplied2 = $partiallyApplied->flatMap(fn ($pa) => $times2->map($pa)); 40 | ``` 41 | 42 | API provides factories to curry functions with up to 30 arguments. Reason to have factories by number of arguments is to provide type safety. 43 | If you use phpstan (which we strongly recommend), it can check all input parameters. 44 | 45 | ## Composition 46 | 47 | Single argument functions are easy to compose. We provide `map` function for this purpose. 48 | 49 | ```php 50 | $plusOne = fn (int $i): int => $i + 1; 51 | $timesTwo = fn (int $i): int => $i * 2; 52 | 53 | $plusOneThenTimesTwo = CurriedFunction::of($plusOne)->map($timesTwo); 54 | $plusOneThenTimesTwo(5); // 12 55 | ``` 56 | 57 | `$f->map($g)` is equivalent of `g ∘ f` or `g(f(x))` in mathematics notation. 58 | 59 | ```php 60 | $composed1 = $f->map($g); 61 | $composed2 = fn ($x) => $g($f($x)); 62 | // $composed1 and $composed2 are equivalent 63 | ``` 64 | 65 | With phpstan, it also checks input / output arguments and creates typed version of composed function. 66 | ```php 67 | function foo(string $s): int { 68 | return strlen($s); 69 | } 70 | 71 | /** 72 | * @param int $i 73 | * @return array 74 | */ 75 | function bar(int $i): array { 76 | return array_fill(0, $i, 'a'); 77 | } 78 | // accepts string and returns array. 79 | // composed type is CurriedFunction> which is equivalent of callable(string): array 80 | $foobar = CurriedFunction::of(foo(...))->map(CurriedFunction::of(bar(...))); 81 | $foobar('abc'); // ['a', 'a', 'a'] 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/lift.md: -------------------------------------------------------------------------------- 1 | # Lift operator 2 | 3 | Lifting function into (applicative) context means transforming it so it can operate on values wrapped in that context. 4 | 5 | ## Example 6 | Example of lifting function into `Option` context: 7 | 8 | ```php 9 | use Bonami\Collection\Option; 10 | 11 | // Get our configuration value wrapped in Option, because it can be missing 12 | function getConfig(string $key): Option { 13 | $config = [ 14 | 'host' => "domain.tld", 15 | 'port' => 8080, 16 | ]; 17 | return Option::fromNullable($config[$key] ?? null); 18 | } 19 | 20 | // Our super useful method accepts string host, int port & returns uri string 21 | $createUri = fn (string $host, int $port): string => "https://{$host}:{$port}"; 22 | 23 | // Unfortunately our string & int are wrapped in Option context, what now? 24 | $hostOption = getConfig('host'); 25 | $portOption = getConfig('port'); 26 | 27 | // Lifting method will allow to pass values wrapped in Option! 28 | $createUriLifted = Option::lift2($createUri); 29 | 30 | // Return value is wrapped in Option too 31 | print $createUriLifted($hostOption, $portOption); // Option::some("https://domain.tld:8080") 32 | ``` 33 | 34 | Please note, that `None` behaves very greedily here - if any of arguments passed into 35 | lifted function is `None` the result will be `None` too. 36 | Lifting function into `TrySafe` context works analogously. 37 | 38 | As we said elsewhere `Option` & `ArrayList` are very look-alike. 39 | Both can represent that value is missing or present and on top of that `ArrayList` 40 | can represent that there is more than one value. 41 | We can do some pretty sick tricks using function lifted in `ArrayList` (`LazyList`) context: 42 | 43 | ```php 44 | use Bonami\Collection\ArrayList; 45 | 46 | $hosts = ArrayList::fromIterable(['foo.org', 'bar.io']); 47 | $ports = ArrayList::fromIterable([80, 8080]); 48 | 49 | $createUri = fn (string $host, int $port): string => "https://{$host}:{$port}"; 50 | 51 | // Accepts list of strings, list of ints & returns list of uris created from all possible combinations 52 | $createUriLifted = ArrayList::lift($createUri); 53 | 54 | print $createUriLifted($hosts, $ports); // [https://foo.org:80, https://bar.io:80, https://foo.org:8080, https://bar.io:8080] 55 | 56 | ``` 57 | 58 | Lifted function generates all possible combinations of hosts & ports for us, which can be very handy! 59 | 60 | Please note that empty `ArrayList` would behave the same way as `None` in `Option` context: 61 | ```php 62 | use Bonami\Collection\ArrayList; 63 | 64 | $hosts = ArrayList::fromIterable(['foo.org', 'bar.io']); 65 | $ports = ArrayList::fromEmpty(); 66 | 67 | $createUri = fn (string $host, int $port): string => "https://{$host}:{$port}"; 68 | 69 | $createUriLifted = ArrayList::lift($createUri); 70 | 71 | print $createUriLifted($hosts, $ports); // [] 72 | ``` 73 | 74 | This is not coincidence - `Option::none()`, `ArrayList::fromEmpty()`, `LazyList::fromEmpty()`, `TrySafe::failure($ex)`, `Either::left($error)` 75 | they all create an instance expressing nonexistence of value and are analogous to zero in context of multiplication 76 | (empty set in cartesian product for these type classes actually). 77 | 78 | All this "common" behavior is similar, because they are all Monads and uses `Applicative` and `Monad` traits. 79 | For more information see [type classes](./type-classes.md) doc 80 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 0.4.x 8 | pull_request: 9 | 10 | jobs: 11 | tests: 12 | name: "Tests" 13 | runs-on: "ubuntu-latest" 14 | 15 | strategy: 16 | matrix: 17 | php: ['8.1', '8.2', '8.3', '8.4'] 18 | 19 | steps: 20 | - name: "Checkout" 21 | uses: "actions/checkout@v2.0.0" 22 | 23 | - name: "Install PHP" 24 | uses: "shivammathur/setup-php@v2" 25 | with: 26 | php-version: "${{ matrix.php }}" 27 | extensions: "json, dom, mbstring" 28 | 29 | - name: "Cache dependencies" 30 | uses: "actions/cache@v1.1.2" 31 | with: 32 | path: "~/.composer/cache" 33 | key: "php-${{ matrix.php }}-composer-cache-${{ hashFiles('**/composer.json') }}" 34 | restore-keys: "php-${{ matrix.php }}-composer-cache" 35 | 36 | - name: "Install dependencies" 37 | run: "composer update --no-suggest --no-interaction --prefer-dist --no-progress" 38 | 39 | - name: "Run tests" 40 | run: "php -d error_reporting=-1 bin/phpunit --colors=always -c phpunit.xml" 41 | 42 | static-analysis: 43 | name: "PHPStan" 44 | runs-on: "ubuntu-latest" 45 | 46 | strategy: 47 | matrix: 48 | php: ['8.1', '8.2', '8.3', '8.4'] 49 | 50 | steps: 51 | - name: "Checkout" 52 | uses: "actions/checkout@v2.0.0" 53 | 54 | - name: "Install PHP" 55 | uses: "shivammathur/setup-php@v2" 56 | with: 57 | php-version: "${{ matrix.php }}" 58 | extensions: "json, dom, mbstring" 59 | 60 | - name: "Cache dependencies" 61 | uses: "actions/cache@v1.1.2" 62 | with: 63 | path: "~/.composer/cache" 64 | key: "php-${{ matrix.php }}-composer-cache-${{ hashFiles('**/composer.json') }}" 65 | restore-keys: "php-${{ matrix.php }}-composer-cache" 66 | 67 | - name: "Install dependencies" 68 | run: "composer update --no-suggest --no-interaction --prefer-dist --no-progress" 69 | 70 | - name: "PHPStan" 71 | run: "php -d error_reporting=-1 -d memory_limit=-1 bin/phpstan --ansi analyse" 72 | 73 | coding-standards: 74 | name: "Coding Standard" 75 | runs-on: "ubuntu-latest" 76 | 77 | strategy: 78 | matrix: 79 | php: ['8.4'] 80 | 81 | steps: 82 | - name: "Checkout" 83 | uses: "actions/checkout@v2.0.0" 84 | 85 | - name: "Install PHP" 86 | uses: "shivammathur/setup-php@v2" 87 | with: 88 | php-version: "${{ matrix.php }}" 89 | extensions: "json, dom, mbstring" 90 | 91 | - name: "Validate Composer" 92 | run: "composer validate" 93 | 94 | - name: "Cache dependencies" 95 | uses: "actions/cache@v1.1.2" 96 | with: 97 | path: "~/.composer/cache" 98 | key: "php-${{ matrix.php }}-composer-cache-${{ hashFiles('**/composer.json') }}" 99 | restore-keys: "php-${{ matrix.php }}-composer-cache" 100 | 101 | - name: "Install dependencies" 102 | run: "composer update --no-suggest --no-interaction --prefer-dist --no-progress" 103 | 104 | - name: "Normalize composer" 105 | run: "composer normalize" 106 | 107 | - name: "Check code styles" 108 | run: "php bin/phpcs --standard=./ruleset.xml --extensions=php --tab-width=4 -sp ./src ./tests" 109 | -------------------------------------------------------------------------------- /docs/type-classes.md: -------------------------------------------------------------------------------- 1 | # Type classes 2 | 3 | Type class is bigger set of classes, that has some common behavior we wont to abstract. 4 | Php has very basic type system and even with phpstan it is hard to do encode it somehow meaningfully. 5 | 6 | Ideally we would have higher kinder generics, which we haven't. So we chose traits, because we can 7 | exploit the way how they are mixed in. We can use `self` and `static` keyword in those traits and they 8 | are bounded to the class they are mixed into. 9 | 10 | Also we can mixin multiple type-class trairs into class, which is something we actually want to do. 11 | 12 | Type-class traits we define: 13 | - `\Bonami\Collection\Applicative1` applicative with 1 hole for generic type 14 | - `\Bonami\Collection\Applicative2` applicative with 2 holes for generic types 15 | - `\Bonami\Collection\Monad1` monad with 1 hole for generic type, uses `\Bonami\Collection\Applicative1` 16 | - `\Bonami\Collection\Monad2` monad with 2 holes for generic types, uses `\Bonami\Collection\Monad2` 17 | - `\Bonami\Collection\Iterable1` iterable type with 1 hole for generic type 18 | 19 | ## Applicative 20 | 21 | Applicative defines abstract methods: 22 | - `pure` - wraps single value into `Applicative` instance. Or more theoretically: pulls impure value into `Applicative` instance context. 23 | - `product` - creates product of two values in context of `Applicative`. Simply put, combines to values into array tupple wrapped in `Applicative` instance 24 | - `map` - maps over values in context of `Applicative` 25 | 26 | Class that mixin this trait needs to implement them. It should implement them in a way, that they obey applicative laws. 27 | 28 | If the class do, it also gain these methods for free: 29 | - `lift1` - `lift30` - factory for augmenting callable to accept and return values wrapped in `Applicative` context. Supports up to 30 fixed arguments. 30 | - `lift` - generic version of lift above. The generic version has worse type safety checks. 31 | - `sequence` - operation to combine multiple `Applicative` instances into single `Applicative` instances containg multiple values (in `ArrayList`). 32 | - `traverse` - similar to `sequence` allowing transformation of the values along the way. 33 | - `ap` - applies single value to callback in context of `Applicative` 34 | 35 | ## Monad 36 | 37 | `Monad` uses `Applicative` trait and defines one extra abstract method: 38 | - `flatMap` - chains operation on `Monad` 39 | 40 | Class that mixin this trait needs to implement all methods from `Monad` and `Applicative` as well except `Applicative::ap` method, 41 | that is already implemented in `Monad`. 42 | 43 | ## Iterable 44 | 45 | Iterable defines only 2 abstract methods: 46 | - `reduce` - for reducing structure to single value 47 | - `getIterator` - a method compatible with `\IteratorAggregate` iterface 48 | 49 | From those 2 simple methods we can derive lots of useful methods for free: 50 | - `mfold` - left folding via `\Bonami\Collection\Monoid\Monoid` instance 51 | - `sum` - transforms item to integer / float and then sums them together 52 | - `find` - finds item by predicate 53 | - `head` - gets very first item if present 54 | - `last` - gets very last item if present 55 | - `min` - gets minimal item by comparator 56 | - `max`- gets maximal item by comparator 57 | - `exists` - checks if at least one element matches the predicate 58 | - `all` - checks if all elements matches the predicate 59 | - `each` - executes side effect on each element 60 | - `tap` - executes side effect on each element and return self 61 | - `toArray` - converts structure to `array` 62 | - `toList` - converts structure to `\Bonami\Collection\ArrayList` 63 | - `count` - counts number of elements 64 | - `isEmpty` - checks if the structure is empty 65 | - `isNotEmpty` - checks if the structure is not empty 66 | 67 | ## Overriding methods 68 | 69 | Sometimes concrete type-class instance (the class that uses type-class trait) can have more optimal version of 70 | "derived" method. Mixing trait into does not forbid overriding it with more optimal version. 71 | 72 | ## Caveats of traits 73 | 74 | They do surprisingly good job for us, but they have one big drawback: they are not types themselves. 75 | 76 | This mean we cannot use them anywhere in code in typehints and we cannot check object instances if 77 | they are `instanceof` some concrete trait. 78 | -------------------------------------------------------------------------------- /docs/try-safe.md: -------------------------------------------------------------------------------- 1 | # TrySafe 2 | 3 | What [`Option`](./option.md) is for possible missing (nullable) values, `TrySafe` is for possible exceptions. 4 | 5 | Traditional way of throwing exceptions and catching them somewhere may obscure that the code can fail. 6 | `TrySafe` is designed (among other things) to be explicit about it on type level. 7 | 8 | ## Example 9 | Consider the following code: 10 | ```php 11 | interface IntegerParser { 12 | public function parse(mixed $input): int; 13 | } 14 | ``` 15 | For well-behaved inputs it will return `int` according to return type declaration. 16 | Until we pass something that we cannot parse. 17 | 18 | As you would guess, `parse` will throw some `Exception` when parsing fails. 19 | We can "improve" the code a little with explicit `@throws` php doc annotation, 20 | but that's about it. Nothing forces us to somehow handle the exceptional case on client side. 21 | 22 | Even typical static analysis don't care too much, because there is nothing like "checked exceptions" 23 | (like in Java). 24 | 25 | ```php 26 | $result = IntegerParser::parse("123") + IntegerParse::parse("foo"); 27 | ``` 28 | 29 | Another problem is, that it fails only "sometime". 30 | Conditional failing is hell for debugging, it is easier to debug something, 31 | that fails always if not handled correctly. 32 | 33 | Let's see how `TrySafe` can help 34 | 35 | ```php 36 | use Bonami\Collection\TrySafe; 37 | 38 | interface IntegerParser { 39 | /** 40 | * @param mixed $input 41 | * @return TrySafe 42 | */ 43 | public function parse(mixed $input): TrySafe; 44 | } 45 | ``` 46 | 47 | This time we know, that parsing can fail directly from signature. What's better, 48 | we cannot access the int directly without handling possible failure as well! 49 | 50 | ```php 51 | $result = IntegerParser::parse("123") 52 | ->flatMap(fn (int $a) => IntegerParse::parse("foo")->map(fn (int $b) => $a + $b)); 53 | ``` 54 | 55 | The example above does not look that much pretty at first glance 56 | (when we need to treat multiple instances of `TrySafe`). 57 | 58 | Fortunately we have more ways of writing this. For example this way: 59 | ```php 60 | use Bonami\Collection\ArrayList; 61 | use Bonami\Collection\identity; 62 | 63 | $result = ArrayList::of("123", "foo") 64 | ->flatMap(IntegerParse::parse(...)) 65 | ->sum(identity()); 66 | ``` 67 | 68 | Or this way: 69 | ```php 70 | use Bonami\Collection\TrySafe; 71 | 72 | $result = TrySafe::lift2(fn (int $a, int $b) => $a + $b)( 73 | IntegerParser::parse("123"), 74 | IntegerParser::parse("foo"), 75 | ); 76 | ``` 77 | 78 | ## Recovery 79 | 80 | We have already learned, that `TrySafe` can be used for chaining dependent operations that can fail (via `flatMap`). 81 | How about having some fallback / recovery in the middle of that chain? 82 | 83 | This is where `recover*` methods come in handy. Let's take a look at this example: 84 | 85 | ```php 86 | /** @var TrySafe */ 87 | $distance = $api 88 | ->findGps($query) 89 | ->recoverWith(fn (Throwable $ex): TrySafe => $backupApi->findGps($query)) 90 | ->map(fn (Gps $gps) => $this->getDistance($home)); 91 | ``` 92 | 93 | There are four `recover*` methods: 94 | - `recover` - Use it to recover with value directly 95 | - `recoverWith` - Use it to recover with value wrapped in `TrySafe`. That allows chaining multiple `failure` recoveries, likewise `flatMap` does for `success`. 96 | - `recoverIf` - Same as `recover`, except it recovers failure only if passed predicate evaluates to true. 97 | - `recoverWithIf` - Same as `recoverWith`, except it recovers failure only if passed predicate evaluates to true. 98 | 99 | ```php 100 | /** @var TrySafe */ 101 | $distance = $api 102 | ->findGps($query) 103 | ->recoverWithIf( 104 | fn (Throwable $ex) => $ex instanceof ConnectionFailure, // recovers only if first api is down 105 | fn (Throwable $ex): TrySafe => $backupApi->findGps($query), 106 | ) 107 | ->recoverIf( 108 | fn (Throwable $ex) => $ex instanceof MalformedQuery, // rather the recovery it keeps as more specific failure 109 | fn (Throwable $ex) => throw new CannotGetGps("Query $query was malformed", 0, $ex), 110 | ) 111 | ->map(fn (Gps $gps) => $this->getDistance($home)); 112 | ``` 113 | -------------------------------------------------------------------------------- /docs/option.md: -------------------------------------------------------------------------------- 1 | # Option 2 | 3 | > aka how to avoid billion dollar mistake by using `Option` (we are looking in your direction, `null`!) 4 | 5 | `Option` type encapsulates value, which may or may not exist. If you are not familiar with concept of `Option` (also called `Maybe` in some languages), think of `ArrayList` which is either empty or has single item inside. 6 | 7 | Value which exists is represented in `some` instance, whereas missing one is `none`. 8 | 9 | ```php 10 | use Bonami\Collection\Option; 11 | 12 | $somethingToEat = Option::some("ice cream"); 13 | $nothingToSeeHere = Option::none(); 14 | ``` 15 | 16 | The good thing is that we can operate on `some` & `none` the same way: 17 | 18 | ```php 19 | use Bonami\Collection\Option; 20 | 21 | $somethingToEat = Option::some("ice cream"); 22 | $nothingToSeeHere = Option::none(); 23 | 24 | $iLikeToEat = fn (string $food): string => "I like to eat tasty {$food}!"; 25 | 26 | $somethingToEat->map($iLikeToEat); // Will map to string "I like to eat tasty ice cream!" wrapped in `some` instance 27 | $nothingToSeeHere->map($iLikeToEat); // `none`, wont be mapped and will stay the same 28 | ``` 29 | 30 | We can use `Option` as better and more safe alternative to nullable values since handling of `null` may easily become cumbersome. 31 | 32 | ## Example 33 | 34 | Imagine we have some (dummy) functions like this: 35 | 36 | ```php 37 | function getUserEmailById(int $id): ?string { 38 | $usersDb = [ 39 | 1 => "john@foobar.baz", 40 | 2 => "paul@foobar.baz", 41 | ]; 42 | return $usersDb[$id] ?? null; 43 | } 44 | function getAgeByUserEmail(string $email): ?int { 45 | $ageDb = [ 46 | "john@foobar.baz" => 66, 47 | "diego@hola.esp" => 42, 48 | ]; 49 | return $ageDb[$email] ?? null; 50 | } 51 | ``` 52 | 53 | Classical way to combine these `with` null will look something like this: 54 | 55 | ```php 56 | function printUserAgeById(int $id): void { 57 | $email = getUserEmailById($id); 58 | $age = null; 59 | if ($email !== null) { 60 | $age = getAgeByUserEmail($email); 61 | 62 | } 63 | if ($age === null) { 64 | print "Dont know age of user with id {$id}"; 65 | } else { 66 | print "Age of user with id {$id} is {$age}"; 67 | } 68 | } 69 | ``` 70 | 71 | With `Option` we can do better: 72 | 73 | ```php 74 | function printUserAgeById(int $id): void { 75 | print Option::fromNullable(getUserEmailById($id)) 76 | ->flatMap(Option::fromNullable(getAgeByUserEmail(...)) 77 | ->map(fn (int $age): string => "Age of user with id {$id} is {$age}") 78 | ->getOrElse("Dont know age of user with id {$id}"); 79 | } 80 | ``` 81 | 82 | Or we can design our methods to work with `Option` in a first place: 83 | ```php 84 | /** 85 | * @param int $id 86 | * @return Option 87 | */ 88 | function getUserEmailById(int $id): Option { 89 | $usersDb = [ 90 | 1 => "john@foobar.baz", 91 | 2 => "paul@foobar.baz", 92 | ]; 93 | return Option::fromNullable($usersDb[$id] ?? null); 94 | } 95 | /** 96 | * @param string $email 97 | * @return Option 98 | */ 99 | function getAgeByUserEmail(string $email): Option { 100 | $ageDb = [ 101 | "john@foobar.baz" => 66, 102 | "diego@hola.esp" => 42, 103 | ]; 104 | return Option::fromNullable($ageDb[$email] ?? null); 105 | } 106 | ``` 107 | 108 | And then: 109 | 110 | ```php 111 | function printUserAgeById(int $id): void { 112 | print getUserEmailById($id) 113 | ->flatMap(getAgeByUserEmail(...)) 114 | ->map(fn (int $age): string => "Age of user with id {$id} is {$age}") 115 | ->getOrElse("Dont know age of user with id {$id}"); 116 | } 117 | ``` 118 | 119 | You can see that the example using `Option` allows us to sequence (chain) computations so that if 120 | any of intermediate steps yields `none`, the subsequent computations are simply ignored. 121 | We hope you have a grasp of it, even though example is rather artificial ;-) 122 | 123 | ## Type classes 124 | 125 | Option mixes `Applicative1`, `Monad1` and `Iterable1` traits. 126 | If you don't know, what [type-class](./type-classes.md) is, don't despair. It simply means, 127 | that it has some common behaviour with other structures and that it has quite rich interface 128 | of methods that it gets (from those traits). 129 | 130 | In case you are a functional programming zealot, you'd like to hear that `Option` 131 | is a lawful monad (thus functor & applicative). 132 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Enum.php: -------------------------------------------------------------------------------- 1 | > */ 18 | private static $instances = []; 19 | 20 | /** @var array> */ 21 | private static $instanceIndex; 22 | 23 | /** @var null|array, array> */ 24 | private static $constNameIndex; 25 | 26 | /** @var int|string */ 27 | private $value; 28 | 29 | /** @param int|string $value */ 30 | final private function __construct($value) 31 | { 32 | $this->value = $value; 33 | } 34 | 35 | /** 36 | * @param mixed $value 37 | * 38 | * @return static 39 | */ 40 | public static function create($value) 41 | { 42 | $class = static::class; 43 | if (is_object($value)) { 44 | throw new InvalidEnumValueException($value, static::class); 45 | } 46 | if (!isset(self::$instanceIndex[$class])) { 47 | $instances = self::instanceList(); 48 | $combined = array_combine($instances->getValues(), $instances->toArray()); 49 | self::$instanceIndex[$class] = $combined; 50 | } 51 | if (!isset(self::$instanceIndex[$class][$value])) { 52 | throw new InvalidEnumValueException($value, static::class); 53 | } 54 | 55 | return self::$instanceIndex[$class][$value]; 56 | } 57 | 58 | /** @return EnumList */ 59 | public static function instanceList(): EnumList 60 | { 61 | // @phpstan-ignore-next-line 62 | return EnumList::fromIterable(self::instanceMap()->values()); 63 | } 64 | 65 | /** @return Map */ 66 | public static function instanceMap(): Map 67 | { 68 | $class = static::class; 69 | 70 | if (isset(self::$instances[$class])) { 71 | return self::$instances[$class]; 72 | } 73 | 74 | /** @var iterable $pairs */ 75 | $pairs = array_map( 76 | static fn ($value) => [$value, new static($value)], 77 | self::getClassConstants(), 78 | ); 79 | 80 | return self::$instances[$class] = Map::fromIterable($pairs); 81 | } 82 | 83 | /** @return array */ 84 | private static function getClassConstants(): array 85 | { 86 | return (new ReflectionClass(static::class))->getConstants(); 87 | } 88 | 89 | /** 90 | * @param static ...$enums 91 | * 92 | * @return EnumList 93 | */ 94 | public static function getListComplement(self ...$enums) 95 | { 96 | // @phpstan-ignore-next-line 97 | return self::instanceList()->minus($enums); 98 | } 99 | 100 | /** 101 | * @param int|string $value 102 | * 103 | * @return bool 104 | */ 105 | public static function exists($value): bool 106 | { 107 | return static::instanceMap()->has($value); 108 | } 109 | 110 | public function getConstName(): string 111 | { 112 | self::lazyInitConstNameIndex(); 113 | 114 | assert(self::$constNameIndex !== null); 115 | return self::$constNameIndex[static::class][$this->value]; 116 | } 117 | 118 | private static function lazyInitConstNameIndex(): void 119 | { 120 | $class = static::class; 121 | if (!isset(self::$constNameIndex)) { 122 | self::$constNameIndex = []; 123 | } 124 | 125 | if (!isset(self::$constNameIndex[$class])) { 126 | $constNameIndex = []; 127 | foreach (self::getClassConstants() as $constName => $value) { 128 | $constNameIndex[$value] = $constName; 129 | } 130 | self::$constNameIndex[$class] = $constNameIndex; 131 | } 132 | } 133 | 134 | public function __toString() 135 | { 136 | return (string)$this->getValue(); 137 | } 138 | 139 | /** @return int|string */ 140 | public function getValue() 141 | { 142 | return $this->value; 143 | } 144 | 145 | public function hashCode(): int|string 146 | { 147 | return $this->getValue(); 148 | } 149 | 150 | public function jsonSerialize(): string 151 | { 152 | return (string)$this->value; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/helpers.php: -------------------------------------------------------------------------------- 1 | $a; 69 | $assertEquals( 70 | $functor, 71 | $functor->map($id), 72 | ); 73 | } 74 | 75 | /** 76 | * @phpstan-param callable(mixed, mixed): void $assertEquals 77 | * @phpstan-param mixed $functor - this should implement some generic functor interface 78 | * @phpstan-param CurriedFunction $f 79 | * @phpstan-param CurriedFunction $g 80 | * 81 | * @phpstan-return void 82 | */ 83 | function testFunctorComposition(callable $assertEquals, $functor, CurriedFunction $f, CurriedFunction $g): void 84 | { 85 | $assertEquals( 86 | $functor->map($g)->map($f), 87 | $functor->map($g->map($f)), 88 | ); 89 | } 90 | 91 | /* @see https://en.wikibooks.org/wiki/Haskell/Applicative_functors#Applicative_functor_laws */ 92 | 93 | /** 94 | * @phpstan-param callable(mixed, mixed): void $assertEquals 95 | * @phpstan-param callable(mixed, mixed): mixed $ap 96 | * @phpstan-param callable(mixed): mixed $pure 97 | * @phpstan-param mixed $applicative - this should implement some generic applicative interface 98 | * 99 | * @phpstan-return void 100 | */ 101 | function testApplicativeIdentity(callable $assertEquals, callable $ap, callable $pure, $applicative): void 102 | { 103 | $assertEquals( 104 | $ap($pure(CurriedFunction::of(id(...))), $applicative), 105 | $applicative, 106 | ); 107 | } 108 | 109 | /** 110 | * @phpstan-param callable(mixed, mixed): void $assertEquals 111 | * @phpstan-param callable(mixed, mixed): mixed $ap 112 | * @phpstan-param callable(mixed): mixed $pure 113 | * @phpstan-param mixed $value 114 | * @phpstan-param CurriedFunction $f 115 | * 116 | * @phpstan-return void 117 | */ 118 | function testApplicativeHomomorphism( 119 | callable $assertEquals, 120 | callable $ap, 121 | callable $pure, 122 | $value, 123 | CurriedFunction $f, 124 | ): void { 125 | $assertEquals( 126 | $ap($pure($f), $pure($value)), 127 | $pure($f($value)), 128 | ); 129 | } 130 | 131 | /** 132 | * @phpstan-param callable(mixed, mixed): void $assertEquals 133 | * @phpstan-param callable(mixed, mixed): mixed $ap 134 | * @phpstan-param callable(mixed): mixed $pure 135 | * @phpstan-param mixed $value 136 | * @phpstan-param mixed $applicativeF - this should implement some generic applicative interface 137 | * 138 | * @phpstan-return void 139 | */ 140 | function testApplicativeInterchange(callable $assertEquals, callable $ap, callable $pure, $value, $applicativeF): void 141 | { 142 | $assertEquals( 143 | $ap($applicativeF, $pure($value)), 144 | $ap($pure(CurriedFunction::of(applicator1($value))), $applicativeF), 145 | ); 146 | } 147 | 148 | /** 149 | * @phpstan-param callable(mixed, mixed): void $assertEquals 150 | * @phpstan-param callable(mixed, mixed): mixed $ap 151 | * @phpstan-param callable(mixed): mixed $pure 152 | * @phpstan-param mixed $applicative - this should implement some generic applicative interface 153 | * @phpstan-param mixed $applicativeF - this should implement some generic applicative interface 154 | * @phpstan-param mixed $applicativeG - this should implement some generic applicative interface 155 | * 156 | * @phpstan-return void 157 | */ 158 | function testApplicativeComposition( 159 | callable $assertEquals, 160 | callable $ap, 161 | callable $pure, 162 | $applicative, 163 | $applicativeF, 164 | $applicativeG, 165 | ): void { 166 | $curriedComposition = CurriedFunction::curry2( 167 | static fn (CurriedFunction $f, CurriedFunction $g): callable => $g->map($f), 168 | ); 169 | 170 | $assertEquals( 171 | $ap($ap($ap($pure($curriedComposition), $applicativeF), $applicativeG), $applicative), 172 | $ap($applicativeF, $ap($applicativeG, $applicative)), 173 | ); 174 | } 175 | 176 | function createInvokableSpy(): CallSpy 177 | { 178 | return new class implements CallSpy { 179 | /** @var array> $calls */ 180 | private $calls = []; 181 | 182 | public function __invoke(): void 183 | { 184 | $this->calls[] = func_get_args(); 185 | } 186 | 187 | public function getCalls(): array 188 | { 189 | return $this->calls; 190 | } 191 | }; 192 | } 193 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Iterable1.php: -------------------------------------------------------------------------------- 1 | mixed 21 | * @param R $initialReduction - initial value used as seed for $carry 22 | * 23 | * @return R - reduced values. If the list is empty, $initialReduction is directly returned 24 | */ 25 | abstract public function reduce(callable $reducer, $initialReduction); 26 | 27 | /** @return Iterator */ 28 | abstract public function getIterator(): Iterator; 29 | 30 | /** 31 | * Reduce (folds) to single value using Monoid 32 | * 33 | * Complexity: o(n) 34 | * 35 | * @see sum - for trivial summing 36 | * 37 | * @param Monoid $monoid 38 | * 39 | * @return T 40 | */ 41 | public function mfold(Monoid $monoid) 42 | { 43 | return $this->reduce(static fn ($carry, $next) => $monoid->concat($carry, $next), $monoid->getEmpty()); 44 | } 45 | 46 | /** 47 | * Converts items to numbers and then sums them up. 48 | * 49 | * Complexity: o(n) 50 | * 51 | * @see mfold - for folding diferent types of items (E.g. classes representing BigNumbers and so on) 52 | * 53 | * @param callable(T): (int|float) $itemToNumber 54 | * 55 | * @return int|float 56 | */ 57 | public function sum(callable $itemToNumber) 58 | { 59 | return $this->reduce(static fn ($carry, $next) => $carry + $itemToNumber($next), 0); 60 | } 61 | 62 | /** 63 | * Finds first item by given predicate where it matches 64 | * 65 | * Complexity: o(n) 66 | * 67 | * @see exists - if you just need to check if something matches by predicate 68 | * @see findKey - if you need to get key by predicate 69 | * 70 | * @param callable(T, int): bool $predicate 71 | * 72 | * @return Option 73 | */ 74 | public function find(callable $predicate): Option 75 | { 76 | foreach ($this->getIterator() as $key => $item) { 77 | if ($predicate($item, $key)) { 78 | return Option::some($item); 79 | } 80 | } 81 | return Option::none(); 82 | } 83 | 84 | /** 85 | * Gets the very first value 86 | * 87 | * Complexity: o(1) 88 | * 89 | * @return Option item wrapped with Option::some or Option::none if empty 90 | */ 91 | public function head(): Option 92 | { 93 | return $this->find(static fn () => true); 94 | } 95 | 96 | /** 97 | * Gets the very last value 98 | * 99 | * Complexity: o(n) 100 | * 101 | * @return Option item wrapped with Option::some or Option::none if empty 102 | */ 103 | public function last(): Option 104 | { 105 | return $this->reduce(static fn ($curry, $next) => Option::of($next), $this->head()); 106 | } 107 | 108 | /** 109 | * Finds minimal value defined by comparator 110 | * 111 | * Complexity: o(n) 112 | * 113 | * @param null|callable(T, T): int $comparator - classic comparator returning 1, 0 or -1 114 | * if no comparator is passed, $first <=> $second is used 115 | * 116 | * @return Option minimal value wrapped in Option::some or Option::none when list is empty 117 | */ 118 | public function min(?callable $comparator = null): Option 119 | { 120 | $comparator ??= comparator(); 121 | $min = Option::lift2(static fn ($a, $b) => $comparator($a, $b) <= 0 ? $a : $b); 122 | return $this->reduce( 123 | static fn ($next, $carry) => $min($next, Option::of($carry)), 124 | $this->head(), 125 | ); 126 | } 127 | 128 | /** 129 | * Finds maximal value defined by comparator 130 | * 131 | * Complexity: o(n) 132 | * 133 | * @param null|callable(T, T): int $comparator - classic comparator returning 1, 0 or -1 134 | * if no comparator is passed, $first <=> $second is used 135 | * 136 | * @return Option minimal value wrapped in Option::some or Option::none when list is empty 137 | */ 138 | public function max(?callable $comparator = null): Option 139 | { 140 | $comparator ??= comparator(); 141 | $max = Option::lift2(static fn ($a, $b) => $comparator($a, $b) > 0 ? $a : $b); 142 | return $this->reduce( 143 | static fn ($next, $carry) => $max($next, Option::of($carry)), 144 | $this->head(), 145 | ); 146 | } 147 | 148 | /** 149 | * Checks if AT LEAST ONE item satisfies predicate. 150 | * 151 | * Complexity: o(n) 152 | * 153 | * @see find - if you need to get item by predicate 154 | * @see all - if you need to check if ALL items in List satisfy predicate 155 | * 156 | * @param callable(T, int): bool $predicate 157 | * 158 | * @return bool 159 | */ 160 | public function exists(callable $predicate): bool 161 | { 162 | return $this->find($predicate)->isDefined(); 163 | } 164 | 165 | /** 166 | * Checks if ALL items satisfy predicate 167 | * 168 | * Complexity: o(n) 169 | * 170 | * @see exists - if you need to check if AT LEAST ONE item in List satisfy predicate 171 | * @see find - if you need to get item by predicate 172 | * 173 | * @param callable(T, int): bool $predicate 174 | * 175 | * @return bool 176 | */ 177 | public function all(callable $predicate): bool 178 | { 179 | return !$this->exists(static fn ($item, int $i) => !$predicate($item, $i)); 180 | } 181 | 182 | /** 183 | * Executes $sideEffect on each item of List 184 | * 185 | * Complexity: o(n) 186 | * 187 | * @param callable(T, int): void $sideEffect 188 | * 189 | * @return void 190 | */ 191 | public function each(callable $sideEffect): void 192 | { 193 | foreach ($this->getIterator() as $key => $item) { 194 | $sideEffect($item, $key); 195 | } 196 | } 197 | 198 | /** 199 | * Executes $sideEffect on each item and returns unchanged instance. 200 | * 201 | * Allows inserting side-effects in a chain of method calls 202 | * 203 | * Complexity: o(n) 204 | * 205 | * @param callable(T, int): void $sideEffect 206 | * 207 | * @return static 208 | */ 209 | public function tap(callable $sideEffect) 210 | { 211 | foreach ($this->getIterator() as $key => $item) { 212 | $sideEffect($item, $key); 213 | } 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Gets classic php native array for interoperability 220 | * 221 | * Complexity: o(1) 222 | * 223 | * @return array 224 | */ 225 | public function toArray(): array 226 | { 227 | return iterator_to_array($this->getIterator(), false); 228 | } 229 | 230 | /** 231 | * Gets classic php native array for interoperability 232 | * 233 | * Complexity: o(1) 234 | * 235 | * @return ArrayList 236 | */ 237 | public function toList(): ArrayList 238 | { 239 | return ArrayList::fromIterable($this->getIterator()); 240 | } 241 | 242 | public function count(): int 243 | { 244 | return count($this->toArray()); 245 | } 246 | 247 | /** 248 | * @see isNotEmpty 249 | * 250 | * @return bool 251 | */ 252 | public function isEmpty(): bool 253 | { 254 | return $this->count() === 0; 255 | } 256 | 257 | /** 258 | * @see isEmpty 259 | * 260 | * @return bool 261 | */ 262 | public function isNotEmpty(): bool 263 | { 264 | return $this->count() !== 0; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collections for PHP with focus on Immutability and Functional Programming 2 | ![Build Status](https://github.com/bonami/collections/workflows/CI/badge.svg) 3 | [![Latest Stable Version](https://poser.pugx.org/bonami/collections/v/stable)](https://packagist.org/packages/bonami/collections) 4 | [![License](https://poser.pugx.org/bonami/collections/license)](https://packagist.org/packages/bonami/collections) 5 | 6 | ## Table of contents 7 | - [Motivation](#motivation) 8 | - [Show me the code!](#motivation) 9 | - [Features](#features) 10 | - [Structures](#structures) 11 | - [Type safety](#type-safety) 12 | - Advanced topics: 13 | - [Type classes](./docs/type-classes.md) 14 | - [Lift operator](./docs/lift.md) 15 | - [Currying](./docs/curried-function.md) 16 | - [Traverse](#traverse) 17 | - [License](#features) 18 | - [Contributing](#features) 19 | 20 | ## Motivation 21 | 22 | Why yet another collections library for PHP? Native PHP arrays or SPL structures like SplFixedArray or SplObjectStorage(and other) are mutable and has very strange interfaces and behaviors. They often represent more data structures at once (eg. SplObjectStorage represents both Set and Map) and theirs interfaces are designed for classic imperative approach. 23 | 24 | We tried to design interfaces of our structures to be focused on declarative approach leveraging functional programing. For more safety, we designed structures to be immutable (we have some mutables as well, because sometime it is necessary for performance reasons) 25 | 26 | All the code is designed to be type safe with [phpstan generics](#type-safety). 27 | 28 | ## Show me the code! 29 | 30 | A code example is worth a thousand words, so here are some simple examples: 31 | 32 | ### Filtering Person DTOs and extracting some information 33 | 34 | ```php 35 | use Bonami\Collection\ArrayList; 36 | 37 | class Person { 38 | 39 | public function __construct( 40 | private readonly string $name, 41 | private readonly int $age 42 | ) {} 43 | 44 | } 45 | 46 | $persons = ArrayList::of(new Person('John', 31), new Person('Jacob', 22), new Person('Arthur', 29)); 47 | $names = $persons 48 | ->filter(fn (Person $person): bool => $person->age <= 30) 49 | ->sort(fn (Person $a, Person $b): int => $a->name <=> $b->name) 50 | ->map(fn (Person $person): string => $person->name) 51 | ->join(";"); 52 | 53 | // $names = "Arthur;Jacob" 54 | ``` 55 | 56 | ### Generating combinations 57 | 58 | ```php 59 | use Bonami\Collection\ArrayList; 60 | 61 | $colors = ArrayList::fromIterable(['red', 'green', 'blue']); 62 | $objects = ArrayList::fromIterable(['car', 'pencil']); 63 | 64 | $coloredObjects = ArrayList::fromIterable($colors) 65 | ->flatMap(fn (string $color) => $objects->map(fn (string $object) => "{$color} {$object}")) 66 | 67 | // $coloredObjects = ArrayList::of('red car', 'red pencil', 'green car', 'green pencil', 'blue car', 'blue pencil') 68 | ``` 69 | 70 | ### Generating combinations with [lift](#lift-operator) 71 | 72 | ```php 73 | use Bonami\Collection\ArrayList; 74 | 75 | $concat = fn (string $first, string $second) => "{$first} {$second}"; 76 | $coloredObjects = ArrayList::lift2($concat)($colors, $objects); 77 | ``` 78 | 79 | ### Character frequency analysis 80 | 81 | ```php 82 | use Bonami\Collection\ArrayList; 83 | use Bonami\Collection\Map; 84 | use function Bonami\Collection\id; 85 | use function Bonami\Collection\descendingComparator; 86 | 87 | function frequencyAnalysis(string $text): Map { 88 | $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY); 89 | return ArrayList::fromIterable($chars) 90 | ->groupBy(id(...)) 91 | ->mapValues(fn (ArrayList $group): int => $group->count()); 92 | } 93 | 94 | $text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras nec mi rhoncus, dignissim tortor ac,' . 95 | ' aliquam metus. Maecenas non hendrerit tellus. Nam molestie augue ac lectus cursus consequat. Nunc ' . 96 | 'ultrices metus sit amet nulla blandit lacinia. Nam vestibulum ultrices mollis. Morbi consequat ante non ' . 97 | 'ornare lobortis. Nullam enim mauris, tempus quis auctor eu, condimentum dignissim nunc. Integer dapibus ' . 98 | 'dolor eu nisl euismod sagittis. Phasellus magna ante, pharetra eget nisi vehicula, elementum lacinia dui. ' . 99 | 'Aliquam semper at eros a sodales. In a rhoncus sapien. Integer blandit volutpat nisl. Donec vitae massa eget ' . 100 | 'mauris dignissim cursus nec et erat. Suspendisse consectetur ac quam sit amet pretium.'; 101 | 102 | // top ten characters by number of occurrences 103 | $top10 = frequencyAnalysis($text) 104 | ->sortValues(descendingComparator()) 105 | ->take(10); 106 | ``` 107 | 108 | ## Features 109 | 110 | ### Structures 111 | 112 | - `\Bonami\Collection\ArrayList` - An immutable (non associative) array wrapper, meant for sequential processing. 113 | - `\Bonami\Collection\Map` - An immutable key-value structure. It can contain any kind of object as keys (with some limitation, see further info in docs). 114 | - `\Bonami\Collection\Mutable\Map` - Mutable variant of Map. 115 | - `\Bonami\Collection\LazyList` - Wrapper on any iterable structure. It leverages yield internally making it lazy. It can save memory significantly. 116 | - [`\Bonami\Collection\Enum`](./docs/enum.md) - Not a collection, but has great synergy with rest of the library. Meant for defining closed enumerations. Provides interesting methods like getting complements list of values for given enum. 117 | - `\Bonami\Collection\EnumList` - List of Enums, extending ArrayList 118 | - [`\Bonami\Collection\Option`](./docs/option.md) - Immutable structure for representing, that you maybe have value and maybe not. It provides safe (functional) approach to handle null pointer errors. 119 | - [`\Bonami\Collection\TrySafe`](./docs/try-safe.md) - Immutable structure for representing, that you have value or error generated upon the way. It provides safe (functional) approach to handle errors without side effects. 120 | - [`\Bonami\Collection\CurriedFunction`](./docs/curried-function.md) - Represents single argument function. It can create curried version of multi argument function, which is better for some function programming composition patterns. 121 | 122 | ### Type safety 123 | 124 | We are using [phpstan](https://phpstan.org/) annotations for better type safety, utilizing generics. For even better type 125 | resolving, we created optional dependency [phpstan-collections](https://github.com/bonami/phpstan-collections), which we strongly 126 | suggest installing if you use phpstan. It fixes some type resolving, especially for late static binding. 127 | 128 | ### Traverse 129 | 130 | You may find yourself in situation, where you map list using mapper function which returns values wrapped in `Option` but you'd rather have values unwrapped. And that is when `traverse` method comes handy: 131 | 132 | ```php 133 | use Bonami\Collection\ArrayList; 134 | use Bonami\Collection\Option; 135 | 136 | $getUserNameById = function(int $id): Option { 137 | $userNamesById = [ 138 | 1 => "John", 139 | 2 => "Paul", 140 | 3 => "George", 141 | 4 => "Ringo", 142 | ]; 143 | return Option::fromNullable($userNamesById[$id] ?? null); 144 | }; 145 | 146 | print Option::traverse(ArrayList::fromIterable([1, 3, 4]), $getUserNameById); 147 | // Some([John, Paul, Ringo]) 148 | ``` 149 | 150 | Compare the result with usage of our old buddy `ArrayList::map`: 151 | 152 | ```php 153 | use Bonami\Collection\ArrayList; 154 | use Bonami\Collection\Option; 155 | 156 | $getUserNameById = function(int $id): Option { 157 | $userNamesById = [ 158 | 1 => "John", 159 | 2 => "Paul", 160 | 3 => "George", 161 | 4 => "Ringo", 162 | ]; 163 | return Option::fromNullable($userNamesById[$id] ?? null); 164 | }; 165 | 166 | print ArrayList::fromIterable([1, 3, 4]) 167 | ->map($getUserNameById); 168 | // [Some(John), Some(George), Some(Ringo)] 169 | ``` 170 | 171 | Did you spot the difference? We have list of options with strings inside here whereas we have option of list with strings inside in the first code example. 172 | 173 | So `traverse` allows us to convert list of `Options` to `Option` of list with unwrapped values. And guess what - as usual, `None` will ruin everything: 174 | 175 | ```php 176 | use Bonami\Collection\ArrayList; 177 | use Bonami\Collection\Option; 178 | 179 | $getUserNameById = function(int $id): Option { 180 | $userNamesById = [ 181 | 1 => "John", 182 | 2 => "Paul", 183 | 3 => "George", 184 | 4 => "Ringo", 185 | ]; 186 | return Option::fromNullable($userNamesById[$id] ?? null); 187 | }; 188 | 189 | print Option::traverse(ArrayList::fromIterable([1, 3, 666]), $getUserNameById); 190 | // None 191 | ``` 192 | 193 | Usage of `traverse` method is not limited to `Option` class. It will work with any applicative, so it is available for `TrySafe`, `ArrayList` & `LazyList` (`Failure` & empty list instances behave the same way as `None`). 194 | 195 | ## License 196 | 197 | This package is released under the [MIT license](LICENSE). 198 | 199 | ## Contributing 200 | 201 | If you wish to contribute to the project, please read the [CONTRIBUTING notes](CONTRIBUTING.md). 202 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Option.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | abstract class Option implements IHashable, IteratorAggregate 21 | { 22 | /** @use Monad1 */ 23 | use Monad1; 24 | /** @use Iterable1 */ 25 | use Iterable1; 26 | 27 | /** @var self|null */ 28 | private static $none; 29 | 30 | /** 31 | * @template V 32 | * 33 | * @param ?V $value 34 | * 35 | * @return self 36 | */ 37 | final public static function fromNullable($value): self 38 | { 39 | return $value === null 40 | ? self::none() 41 | : self::some($value); 42 | } 43 | 44 | /** @return self */ 45 | final public static function none(): Option 46 | { 47 | return self::$none ?? self::$none = new class extends Option { 48 | public function isDefined(): bool 49 | { 50 | return false; 51 | } 52 | 53 | public function isEmpty(): bool 54 | { 55 | return true; 56 | } 57 | 58 | public function each(callable $sideEffect): void 59 | { 60 | } 61 | 62 | public function tapNone(callable $sideEffect): Option 63 | { 64 | $sideEffect(); 65 | return $this; 66 | } 67 | 68 | public function filter(callable $predicate): Option 69 | { 70 | return $this; 71 | } 72 | 73 | /** 74 | * Consider calling getOrElse instead 75 | * 76 | * @throws ValueIsNotPresentException 77 | * 78 | * @return T 79 | */ 80 | public function getUnsafe() 81 | { 82 | throw new ValueIsNotPresentException('Can not get value from None'); 83 | } 84 | 85 | /** 86 | * @template E 87 | * 88 | * @param E $else 89 | * 90 | * @return T|E 91 | */ 92 | public function getOrElse($else) 93 | { 94 | return $else; 95 | } 96 | 97 | public function getOrElseLazy($else) 98 | { 99 | return $else(); 100 | } 101 | 102 | public function getOrThrow(callable $throw) 103 | { 104 | throw $throw(); 105 | } 106 | 107 | public function toTrySafe(): TrySafe 108 | { 109 | return TrySafe::failure(new ValueIsNotPresentException()); 110 | } 111 | 112 | /** 113 | * @template L 114 | * 115 | * @param L $left 116 | * 117 | * @return Either 118 | */ 119 | public function toEither($left): Either 120 | { 121 | return Either::left($left); 122 | } 123 | 124 | public function hashCode(): string 125 | { 126 | return spl_object_hash($this); // There should be only one instance of none 127 | } 128 | 129 | /** @return Iterator */ 130 | public function getIterator(): Iterator 131 | { 132 | return new EmptyIterator(); 133 | } 134 | 135 | public function orElse(Option $else): Option 136 | { 137 | return $else; 138 | } 139 | 140 | public function resolve(callable $handleNone, callable $handleSome) 141 | { 142 | return $handleNone(); 143 | } 144 | 145 | public function __toString(): string 146 | { 147 | return 'None'; 148 | } 149 | }; 150 | } 151 | 152 | /** 153 | * @template V 154 | * 155 | * @param V $value 156 | * 157 | * @return self 158 | */ 159 | final public static function some($value): self 160 | { 161 | return new class ($value) extends Option { 162 | /** @var V */ 163 | private $value; 164 | 165 | /** @param V $value */ 166 | protected function __construct($value) 167 | { 168 | $this->value = $value; 169 | } 170 | 171 | public function isDefined(): bool 172 | { 173 | return true; 174 | } 175 | 176 | public function isEmpty(): bool 177 | { 178 | return false; 179 | } 180 | 181 | public function each(callable $sideEffect): void 182 | { 183 | $sideEffect($this->value); 184 | } 185 | 186 | public function tapNone(callable $sideEffect): Option 187 | { 188 | return $this; 189 | } 190 | 191 | public function filter(callable $predicate): Option 192 | { 193 | return $predicate($this->value) 194 | ? $this 195 | : self::none(); 196 | } 197 | 198 | /** 199 | * Consider calling getOrElse instead 200 | * 201 | * @return V 202 | */ 203 | public function getUnsafe() 204 | { 205 | return $this->value; 206 | } 207 | 208 | /** 209 | * @template E 210 | * 211 | * @param E $else 212 | * 213 | * @return V|E 214 | */ 215 | public function getOrElse($else) 216 | { 217 | return $this->value; 218 | } 219 | 220 | public function getOrElseLazy($else) 221 | { 222 | return $this->value; 223 | } 224 | 225 | public function getOrThrow(callable $throw) 226 | { 227 | return $this->value; 228 | } 229 | 230 | public function toTrySafe(): TrySafe 231 | { 232 | return TrySafe::success($this->value); 233 | } 234 | 235 | /** 236 | * @template L 237 | * 238 | * @param L $left 239 | * 240 | * @return Either 241 | */ 242 | public function toEither($left): Either 243 | { 244 | return Either::right($this->value); 245 | } 246 | 247 | public function hashCode(): string 248 | { 249 | $valueHash = $this->value instanceof IHashable 250 | ? $this->value->hashCode() 251 | : hashKey($this->value); 252 | return sprintf('%s::some(%s)', self::class, $valueHash); 253 | } 254 | 255 | /** @return Iterator */ 256 | public function getIterator(): Iterator 257 | { 258 | return new ArrayIterator([$this->value]); 259 | } 260 | 261 | public function orElse(Option $else): Option 262 | { 263 | return $this; 264 | } 265 | 266 | public function resolve(callable $handleNone, callable $handleSome) 267 | { 268 | return $handleSome($this->value); 269 | } 270 | 271 | public function __toString(): string 272 | { 273 | return 'Some(' . $this->value . ')'; 274 | } 275 | }; 276 | } 277 | 278 | /** 279 | * @template B 280 | * 281 | * @param callable(T): B $mapper 282 | * 283 | * @return self 284 | */ 285 | public function map(callable $mapper): self 286 | { 287 | return $this->isEmpty() 288 | ? $this 289 | : self::some($mapper($this->getUnsafe())); 290 | } 291 | 292 | /** 293 | * @template V 294 | * 295 | * @param V $value 296 | * 297 | * @return self 298 | */ 299 | final public static function of($value) 300 | { 301 | return self::some($value); 302 | } 303 | 304 | /** 305 | * @template V 306 | * 307 | * @param V $value 308 | * 309 | * @return self 310 | */ 311 | final public static function pure($value) 312 | { 313 | return self::some($value); 314 | } 315 | 316 | abstract public function isDefined(): bool; 317 | 318 | abstract public function isEmpty(): bool; 319 | 320 | /** 321 | * @param callable(T): bool $predicate 322 | * 323 | * @return self 324 | */ 325 | abstract public function filter(callable $predicate): self; 326 | 327 | /** 328 | * @template B 329 | * 330 | * @param callable(T): self $mapper 331 | * 332 | * @return self 333 | */ 334 | public function flatMap(callable $mapper): self 335 | { 336 | return $this->isEmpty() 337 | ? self::none() 338 | : $mapper($this->getUnsafe()); 339 | } 340 | 341 | /** 342 | * @template R 343 | * 344 | * @param callable(R, T): R $reducer 345 | * @param R $initialReduction 346 | * 347 | * @return R 348 | */ 349 | final public function reduce(callable $reducer, $initialReduction) 350 | { 351 | return LazyList::fromIterable($this)->reduce($reducer, $initialReduction); 352 | } 353 | 354 | /** @param callable(T): void $sideEffect */ 355 | abstract public function each(callable $sideEffect): void; 356 | 357 | /** 358 | * Executes $sideEffect if Option is some and ignores it for none. Then returns Option unchanged 359 | * (the very same reference) 360 | * 361 | * Allows inserting side-effects in a chain of method calls 362 | * 363 | * Complexity: o(1) 364 | * 365 | * @param callable(T): void $sideEffect 366 | * 367 | * @return self 368 | */ 369 | public function tap(callable $sideEffect): self 370 | { 371 | foreach ($this as $item) { 372 | $sideEffect($item); 373 | } 374 | 375 | return $this; 376 | } 377 | 378 | /** 379 | * Executes $sideEffect if Option is none and ignores it for some. Then returns Option unchanged 380 | * (the very same reference) 381 | * 382 | * Allows inserting side-effects when you want to react on missing value (like logging) 383 | * 384 | * Complexity: o(1) 385 | * 386 | * @param callable(): void $sideEffect 387 | * 388 | * @return self 389 | */ 390 | abstract public function tapNone(callable $sideEffect): self; 391 | 392 | /** 393 | * Consider calling getOrElse instead 394 | * 395 | * @throws ValueIsNotPresentException 396 | * 397 | * @return T 398 | */ 399 | abstract public function getUnsafe(); 400 | 401 | /** 402 | * @template E 403 | * 404 | * @param E $else 405 | * 406 | * @return T|E 407 | */ 408 | abstract public function getOrElse($else); 409 | 410 | /** 411 | * @template E 412 | * 413 | * @param callable(): E $else 414 | * 415 | * @return T|E 416 | */ 417 | abstract public function getOrElseLazy($else); 418 | 419 | /** 420 | * @param callable(): Throwable $throw 421 | * 422 | * @return T 423 | */ 424 | abstract public function getOrThrow(callable $throw); 425 | 426 | /** @return TrySafe */ 427 | abstract public function toTrySafe(): TrySafe; 428 | 429 | /** 430 | * @template L 431 | * 432 | * @param L $left 433 | * 434 | * @return Either 435 | */ 436 | abstract public function toEither($left): Either; 437 | 438 | /** @return ArrayList */ 439 | public function toList(): ArrayList 440 | { 441 | return ArrayList::fromIterable($this); 442 | } 443 | 444 | /** @return array */ 445 | public function toArray(): array 446 | { 447 | return iterator_to_array($this); 448 | } 449 | 450 | /** 451 | * @template E 452 | * 453 | * @param self $else 454 | * 455 | * @return self 456 | */ 457 | abstract public function orElse(self $else): self; 458 | 459 | /** 460 | * @template B 461 | * 462 | * @param callable(): B $handleNone 463 | * @param callable(T): B $handleSome 464 | * 465 | * @return B 466 | */ 467 | abstract public function resolve(callable $handleNone, callable $handleSome); 468 | 469 | /** 470 | * @param self $value 471 | * 472 | * @return bool 473 | */ 474 | final public function equals(self $value): bool 475 | { 476 | return $value->hashCode() === $this->hashCode(); 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/Bonami/Collection/Either.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | abstract class Either implements IHashable, IteratorAggregate 32 | { 33 | /** @use Monad2 */ 34 | use Monad2; 35 | 36 | /** 37 | * @param L $left 38 | * 39 | * @return self 40 | */ 41 | final public static function left($left): self 42 | { 43 | return new class ($left) extends Either { 44 | /** @var L */ 45 | private $left; 46 | 47 | /** @param L $left */ 48 | protected function __construct($left) 49 | { 50 | $this->left = $left; 51 | } 52 | 53 | public function isLeft(): bool 54 | { 55 | return true; 56 | } 57 | 58 | public function isRight(): bool 59 | { 60 | return false; 61 | } 62 | 63 | public function mapLeft(callable $mapper): Either 64 | { 65 | return self::left($mapper($this->left)); 66 | } 67 | 68 | public function each(callable $sideEffect): void 69 | { 70 | } 71 | 72 | public function tapLeft(callable $sideEffect): Either 73 | { 74 | $sideEffect($this->left); 75 | 76 | return $this; 77 | } 78 | 79 | public function exists(callable $predicate): bool 80 | { 81 | return false; 82 | } 83 | 84 | /** 85 | * Consider calling getOrElse instead 86 | * 87 | * @return L 88 | */ 89 | public function getLeftUnsafe() 90 | { 91 | return $this->left; 92 | } 93 | 94 | /** 95 | * Consider calling getOrElse instead 96 | * 97 | * @throws ValueIsNotPresentException 98 | * 99 | * @return R 100 | */ 101 | public function getRightUnsafe() 102 | { 103 | throw new ValueIsNotPresentException('Can not get Right value from Left'); 104 | } 105 | 106 | public function resolve(callable $handleLeft, callable $handleRight) 107 | { 108 | return $handleLeft($this->left); 109 | } 110 | 111 | /** 112 | * @template E 113 | * 114 | * @param E $else 115 | * 116 | * @return R|E 117 | */ 118 | public function getOrElse($else) 119 | { 120 | return $else; 121 | } 122 | 123 | public function toTrySafe(): TrySafe 124 | { 125 | return TrySafe::failure(new ValueIsNotPresentException()); 126 | } 127 | 128 | public function hashCode(): string 129 | { 130 | $valueHash = $this->left instanceof IHashable 131 | ? $this->left->hashCode() 132 | : hashKey($this->left); 133 | return sprintf('%s::left(%s)', self::class, $valueHash); 134 | } 135 | 136 | /** @return Iterator */ 137 | public function getIterator(): Iterator 138 | { 139 | return new EmptyIterator(); 140 | } 141 | 142 | public function orElse(Either $else): Either 143 | { 144 | return $else; 145 | } 146 | 147 | public function equals($other): bool 148 | { 149 | return $other instanceof self && $other->left === $this->left; 150 | } 151 | 152 | public function __toString(): string 153 | { 154 | return 'Either::left(' . $this->left . ')'; 155 | } 156 | 157 | public function toOption(): Option 158 | { 159 | return Option::none(); 160 | } 161 | 162 | public function switch(): Either 163 | { 164 | return Either::right($this->left); 165 | } 166 | }; 167 | } 168 | 169 | /** 170 | * @template V 171 | * 172 | * @param V $right 173 | * 174 | * @return self 175 | */ 176 | final public static function right($right): self 177 | { 178 | return new class ($right) extends Either { 179 | /** @var V */ 180 | private $right; 181 | 182 | /** @param V $right */ 183 | protected function __construct($right) 184 | { 185 | $this->right = $right; 186 | } 187 | 188 | public function isLeft(): bool 189 | { 190 | return false; 191 | } 192 | 193 | public function isRight(): bool 194 | { 195 | return true; 196 | } 197 | 198 | public function mapLeft(callable $mapper): Either 199 | { 200 | return $this; 201 | } 202 | 203 | public function each(callable $sideEffect): void 204 | { 205 | $sideEffect($this->right); 206 | } 207 | 208 | public function tapLeft(callable $sideEffect): Either 209 | { 210 | return $this; 211 | } 212 | 213 | public function exists(callable $predicate): bool 214 | { 215 | return $predicate($this->right); 216 | } 217 | 218 | /** 219 | * Consider calling getOrElse instead 220 | * 221 | * @throws ValueIsNotPresentException 222 | * 223 | * @return L 224 | */ 225 | public function getLeftUnsafe() 226 | { 227 | throw new ValueIsNotPresentException('Can not get Left value from Right'); 228 | } 229 | 230 | /** 231 | * Consider calling getOrElse instead 232 | * 233 | * @return V 234 | */ 235 | public function getRightUnsafe() 236 | { 237 | return $this->right; 238 | } 239 | 240 | public function resolve(callable $handleLeft, callable $handleRight) 241 | { 242 | return $handleRight($this->right); 243 | } 244 | 245 | /** 246 | * @template E 247 | * 248 | * @param E $else 249 | * 250 | * @return V|E 251 | */ 252 | public function getOrElse($else) 253 | { 254 | return $this->right; 255 | } 256 | 257 | public function toTrySafe(): TrySafe 258 | { 259 | return TrySafe::success($this->right); 260 | } 261 | 262 | public function hashCode(): string 263 | { 264 | $valueHash = $this->right instanceof IHashable 265 | ? $this->right->hashCode() 266 | : hashKey($this->right); 267 | return sprintf('%s::right(%s)', self::class, $valueHash); 268 | } 269 | 270 | /** @return Iterator */ 271 | public function getIterator(): Iterator 272 | { 273 | return new ArrayIterator([$this->right]); 274 | } 275 | 276 | public function orElse(Either $else): Either 277 | { 278 | return $this; 279 | } 280 | 281 | public function equals($other): bool 282 | { 283 | return $other instanceof self && $other->right === $this->right; 284 | } 285 | 286 | public function __toString(): string 287 | { 288 | return 'Either::right(' . $this->right . ')'; 289 | } 290 | 291 | public function toOption(): Option 292 | { 293 | return Option::some($this->right); 294 | } 295 | 296 | public function switch(): Either 297 | { 298 | return Either::left($this->right); 299 | } 300 | }; 301 | } 302 | 303 | /** 304 | * @template A 305 | * 306 | * @param callable(R): A $mapper 307 | * 308 | * @return self 309 | */ 310 | public function map(callable $mapper): self 311 | { 312 | return $this->isRight() 313 | ? self::right($mapper($this->getRightUnsafe())) 314 | : $this; 315 | } 316 | 317 | /** 318 | * @template B 319 | * 320 | * @param callable(L): B $mapper 321 | * 322 | * @return self 323 | */ 324 | abstract public function mapLeft(callable $mapper): self; 325 | 326 | /** 327 | * @template V 328 | * 329 | * @param V $value 330 | * 331 | * @return self 332 | */ 333 | final public static function of($value): self 334 | { 335 | return self::right($value); 336 | } 337 | 338 | /** 339 | * @template V 340 | * 341 | * @param V $value 342 | * 343 | * @return self 344 | */ 345 | final public static function pure($value): self 346 | { 347 | return self::right($value); 348 | } 349 | 350 | abstract public function isRight(): bool; 351 | 352 | abstract public function isLeft(): bool; 353 | 354 | /** 355 | * @param callable(R): bool $predicate 356 | * 357 | * @return bool 358 | */ 359 | abstract public function exists(callable $predicate): bool; 360 | 361 | /** 362 | * @template B 363 | * 364 | * @param callable(R): self $mapper 365 | * 366 | * @return self 367 | */ 368 | public function flatMap(callable $mapper): self 369 | { 370 | return $this->isRight() 371 | ? $mapper($this->getRightUnsafe()) 372 | : $this; 373 | } 374 | 375 | /** 376 | * @template B 377 | * 378 | * @param callable(L): self $mapper 379 | * 380 | * @return self 381 | */ 382 | public function flatMapLeft(callable $mapper): self 383 | { 384 | return $this->isLeft() 385 | ? $mapper($this->getLeftUnsafe()) 386 | : $this; 387 | } 388 | 389 | /** 390 | * @template A 391 | * 392 | * @param callable(A, R): A $reducer 393 | * @param A $initialReduction 394 | * 395 | * @return A 396 | */ 397 | final public function reduce(callable $reducer, $initialReduction) 398 | { 399 | return LazyList::fromIterable($this)->reduce($reducer, $initialReduction); 400 | } 401 | 402 | /** @param callable(R): void $sideEffect */ 403 | abstract public function each(callable $sideEffect): void; 404 | 405 | /** 406 | * Executes $sideEffect if Either is right and ignores it for left. Then returns Either unchanged 407 | * (the very same reference) 408 | * 409 | * Allows inserting side-effects in a chain of method calls 410 | * 411 | * Complexity: o(1) 412 | * 413 | * @param callable(R): void $sideEffect 414 | * 415 | * @return self 416 | */ 417 | public function tap(callable $sideEffect): self 418 | { 419 | foreach ($this as $item) { 420 | $sideEffect($item); 421 | } 422 | 423 | return $this; 424 | } 425 | 426 | /** 427 | * Executes $sideEffect if Either is left and ignores it for right. Then returns Either unchanged 428 | * (the very same reference) 429 | * 430 | * Allows inserting side-effects in a chain of method calls 431 | * 432 | * Complexity: o(1) 433 | * 434 | * @param callable(L): void $sideEffect 435 | * 436 | * @return self 437 | */ 438 | abstract public function tapLeft(callable $sideEffect): self; 439 | 440 | /** 441 | * Consider calling getOrElse instead 442 | * 443 | * @throws ValueIsNotPresentException 444 | * 445 | * @return L 446 | */ 447 | abstract public function getLeftUnsafe(); 448 | 449 | /** 450 | * Consider calling getOrElse instead 451 | * 452 | * @throws ValueIsNotPresentException 453 | * 454 | * @return R 455 | */ 456 | abstract public function getRightUnsafe(); 457 | 458 | /** 459 | * @template B 460 | * 461 | * @param callable(L): B $handleLeft 462 | * @param callable(R): B $handleRight 463 | * 464 | * @return B 465 | */ 466 | abstract public function resolve(callable $handleLeft, callable $handleRight); 467 | 468 | /** 469 | * @template E 470 | * 471 | * @param E $else 472 | * 473 | * @return R|E 474 | */ 475 | abstract public function getOrElse($else); 476 | 477 | /** 478 | * Converts Either to TrySafe. 479 | * 480 | * Left value is dropped and replaced with exception 481 | * ValueIsNotPresentException wrapped in `TrySafe::failure` 482 | * 483 | * Right value is preserved and wrapped into `TrySafe::success` 484 | * 485 | * @return TrySafe 486 | */ 487 | abstract public function toTrySafe(): TrySafe; 488 | 489 | /** 490 | * Converts Either to Option. 491 | * 492 | * Right value is preserved and wrapped into `Option::some` 493 | * 494 | * Left value is dropped and replaced with `Option::none` 495 | * 496 | * @return Option 497 | */ 498 | abstract public function toOption(): Option; 499 | 500 | /** 501 | * @param self $else 502 | * 503 | * @return self 504 | */ 505 | abstract public function orElse(self $else): self; 506 | 507 | /** 508 | * @param self $other 509 | * 510 | * @return bool 511 | */ 512 | abstract public function equals(self $other): bool; 513 | 514 | /** 515 | * Switches left and right. This can be useful when you need to operate with 516 | * left side same way as it was monad (e.g. flat mapping) 517 | * 518 | * @return self 519 | */ 520 | abstract public function switch(): self; 521 | } 522 | -------------------------------------------------------------------------------- /src/Bonami/Collection/TrySafe.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | abstract class TrySafe implements IHashable, IteratorAggregate 21 | { 22 | /** @use Monad1 */ 23 | use Monad1; 24 | /** @use Iterable1 */ 25 | use Iterable1; 26 | 27 | /** 28 | * @template V 29 | * 30 | * @param V $value 31 | * 32 | * @return self 33 | */ 34 | final public static function of($value): self 35 | { 36 | return self::success($value); 37 | } 38 | 39 | /** 40 | * @template V 41 | * 42 | * @param V $value 43 | * 44 | * @return self 45 | */ 46 | final public static function pure($value): self 47 | { 48 | return self::success($value); 49 | } 50 | 51 | /** 52 | * @template V 53 | * 54 | * @param callable(): V $callable 55 | * 56 | * @return self 57 | */ 58 | final public static function fromCallable(callable $callable): self 59 | { 60 | try { 61 | return self::success($callable()); 62 | } catch (Throwable $failure) { 63 | return self::failure($failure); 64 | } 65 | } 66 | 67 | /** 68 | * @template V 69 | * 70 | * @param V $value 71 | * 72 | * @return self 73 | */ 74 | final public static function success($value): self 75 | { 76 | /** @extends TrySafe */ 77 | return new class ($value) extends TrySafe { 78 | /** @var V */ 79 | private $value; 80 | 81 | /** @param V $value */ 82 | protected function __construct($value) 83 | { 84 | $this->value = $value; 85 | } 86 | 87 | public function isSuccess(): bool 88 | { 89 | return true; 90 | } 91 | 92 | public function mapFailure(callable $exceptionMapper): TrySafe 93 | { 94 | return $this; 95 | } 96 | 97 | public function tapFailure(callable $sideEffect): TrySafe 98 | { 99 | return $this; 100 | } 101 | 102 | /** @inheritDoc */ 103 | public function recover(callable $callable): TrySafe 104 | { 105 | return $this; 106 | } 107 | 108 | /** @inheritDoc */ 109 | public function recoverIf(callable $predicate, callable $recovery): TrySafe 110 | { 111 | return $this; 112 | } 113 | 114 | /** @inheritDoc */ 115 | public function recoverWith(callable $callable): TrySafe 116 | { 117 | return $this; 118 | } 119 | 120 | /** @inheritDoc */ 121 | public function recoverWithIf(callable $predicate, callable $recovery): TrySafe 122 | { 123 | return $this; 124 | } 125 | 126 | /** @return V */ 127 | public function getUnsafe() 128 | { 129 | return $this->value; 130 | } 131 | 132 | /** 133 | * @template E 134 | * 135 | * @param E $else 136 | * 137 | * @return V|E 138 | */ 139 | public function getOrElse($else) 140 | { 141 | return $this->value; 142 | } 143 | 144 | /** @inheritDoc */ 145 | public function getFailureUnsafe(): Throwable 146 | { 147 | throw new ValueIsNotPresentException('Can not get Failure from Success'); 148 | } 149 | 150 | public function toOption(): Option 151 | { 152 | return Option::some($this->value); 153 | } 154 | 155 | public function toEither(): Either 156 | { 157 | return Either::right($this->value); 158 | } 159 | 160 | public function resolve(callable $handleFailure, callable $handleSuccess) 161 | { 162 | return $handleSuccess($this->value); 163 | } 164 | 165 | /** @return Iterator */ 166 | public function getIterator(): Iterator 167 | { 168 | return new ArrayIterator([$this->value]); 169 | } 170 | 171 | public function hashCode(): string 172 | { 173 | $valueHash = $this->value instanceof IHashable 174 | ? $this->value->hashCode() 175 | : hashKey($this->value); 176 | return sprintf('%s::success(%s)', self::class, $valueHash); 177 | } 178 | }; 179 | } 180 | 181 | /** 182 | * @param Throwable $failure 183 | * 184 | * @return self 185 | */ 186 | final public static function failure(Throwable $failure): TrySafe 187 | { 188 | /** @extends TrySafe */ 189 | return new class ($failure) extends TrySafe { 190 | /** @var Throwable */ 191 | private $failure; 192 | 193 | protected function __construct(Throwable $failure) 194 | { 195 | $this->failure = $failure; 196 | } 197 | 198 | public function isSuccess(): bool 199 | { 200 | return false; 201 | } 202 | 203 | public function mapFailure(callable $exceptionMapper): TrySafe 204 | { 205 | return self::failure($exceptionMapper($this->failure)); 206 | } 207 | 208 | public function tapFailure(callable $sideEffect): TrySafe 209 | { 210 | $sideEffect($this->failure); 211 | 212 | return $this; 213 | } 214 | 215 | public function recover(callable $callable): TrySafe 216 | { 217 | return self::fromCallable(fn () => $callable($this->failure)); 218 | } 219 | 220 | /** @inheritDoc */ 221 | public function recoverIf(callable $predicate, callable $recovery): TrySafe 222 | { 223 | if ($predicate($this->failure)) { 224 | return $this->recover($recovery); 225 | } 226 | 227 | return $this; 228 | } 229 | 230 | /** 231 | * @param callable(Throwable): TrySafe $callable 232 | * 233 | * @return TrySafe 234 | */ 235 | public function recoverWith(callable $callable): TrySafe 236 | { 237 | return self::fromCallable(fn() => $callable($this->failure)) 238 | ->flatMap(static fn($x) => $x); 239 | } 240 | 241 | /** @inheritDoc */ 242 | public function recoverWithIf(callable $predicate, callable $recovery): TrySafe 243 | { 244 | if ($predicate($this->failure)) { 245 | return $this->recoverWith($recovery); 246 | } 247 | 248 | return $this; 249 | } 250 | 251 | /** 252 | * Consider calling getOrElse instead 253 | * 254 | * @throws ValueIsNotPresentException 255 | * 256 | * @return T 257 | */ 258 | public function getUnsafe() 259 | { 260 | throw new ValueIsNotPresentException('Can not get value from Failure', 0, $this->failure); 261 | } 262 | 263 | /** 264 | * @template E 265 | * 266 | * @param E $else 267 | * 268 | * @return T|E 269 | */ 270 | public function getOrElse($else) 271 | { 272 | return $else; 273 | } 274 | 275 | public function getFailureUnsafe(): Throwable 276 | { 277 | return $this->failure; 278 | } 279 | 280 | public function toOption(): Option 281 | { 282 | return Option::none(); 283 | } 284 | 285 | public function toEither(): Either 286 | { 287 | return Either::left($this->failure); 288 | } 289 | 290 | public function resolve(callable $handleFailure, callable $handleSuccess) 291 | { 292 | return $handleFailure($this->failure); 293 | } 294 | 295 | /** @return Iterator */ 296 | public function getIterator(): Iterator 297 | { 298 | return new EmptyIterator(); 299 | } 300 | 301 | public function hashCode(): string 302 | { 303 | $failureHash = $this->failure instanceof IHashable 304 | ? $this->failure->hashCode() 305 | : hashKey($this->failure); 306 | return sprintf('%s::failure(%s)', self::class, $failureHash); 307 | } 308 | }; 309 | } 310 | 311 | /** 312 | * Allow mapping failure. This can be useful when you need to translate 313 | * some generic exception into something more domain specific. 314 | * 315 | * @param callable(Throwable): Throwable $exceptionMapper 316 | * 317 | * @return self 318 | */ 319 | abstract public function mapFailure(callable $exceptionMapper): self; 320 | 321 | /** 322 | * @template B 323 | * 324 | * @param callable(T): B $mapper 325 | * 326 | * @return self 327 | */ 328 | final public function map(callable $mapper): self 329 | { 330 | return $this->isSuccess() 331 | ? self::fromCallable(fn() => $mapper($this->getUnsafe())) 332 | : $this; 333 | } 334 | 335 | /** 336 | * @template B 337 | * 338 | * @param callable(T): self $mapper 339 | * 340 | * @return self 341 | */ 342 | public function flatMap(callable $mapper): self 343 | { 344 | if ($this->isSuccess()) { 345 | try { 346 | return $mapper($this->getUnsafe()); 347 | } catch (Throwable $failure) { 348 | return self::failure($failure); 349 | } 350 | } 351 | 352 | return $this; 353 | } 354 | 355 | /** 356 | * Executes $sideEffect if TrySafe is successful and ignores it otherwise 357 | * 358 | * Complexity: o(1) 359 | * 360 | * @param callable(T): void $sideEffect 361 | * 362 | * @return void 363 | */ 364 | public function each(callable $sideEffect): void 365 | { 366 | foreach ($this as $item) { 367 | $sideEffect($item); 368 | } 369 | } 370 | 371 | /** 372 | * Executes $sideEffect if TrySafe is successful and ignores it otherwise. Then returns TrySafe unchanged 373 | * (the very same reference) 374 | * 375 | * Allows inserting side-effects in a chain of method calls 376 | * 377 | * Complexity: o(1) 378 | * 379 | * @param callable(T): void $sideEffect 380 | * 381 | * @return self 382 | */ 383 | public function tap(callable $sideEffect): self 384 | { 385 | foreach ($this as $item) { 386 | $sideEffect($item); 387 | } 388 | 389 | return $this; 390 | } 391 | 392 | /** 393 | * Executes $sideEffect if TrySafe is failure and ignores it otherwise. Then returns TrySafe unchanged 394 | * (the very same reference) 395 | * 396 | * Allows inserting side-effects in a chain of method calls 397 | * 398 | * Complexity: o(1) 399 | * 400 | * @param callable(Throwable): void $sideEffect 401 | * 402 | * @return self 403 | */ 404 | abstract public function tapFailure(callable $sideEffect): self; 405 | 406 | /** 407 | * @template R 408 | * 409 | * @param callable(R, T): R $reducer 410 | * @param R $initialReduction 411 | * 412 | * @return R 413 | */ 414 | final public function reduce(callable $reducer, $initialReduction) 415 | { 416 | return LazyList::fromIterable($this)->reduce($reducer, $initialReduction); 417 | } 418 | 419 | /** 420 | * @param self $value 421 | * 422 | * @return bool 423 | */ 424 | final public function equals(self $value): bool 425 | { 426 | return $value->hashCode() === $this->hashCode(); 427 | } 428 | 429 | /** 430 | * @param callable(Throwable): T $callable 431 | * 432 | * @return self 433 | */ 434 | abstract public function recover(callable $callable): self; 435 | 436 | /** 437 | * Runs recovery only if predicate returns true. E.g. to check type of Throwable. 438 | * 439 | * Recovery must return directly the value or throw another exception 440 | * (which is automatically wrapped into TrySafe) 441 | * 442 | * @param callable(Throwable): bool $predicate 443 | * @param callable(Throwable): T $recovery 444 | * 445 | * @return self 446 | */ 447 | abstract public function recoverIf(callable $predicate, callable $recovery): self; 448 | 449 | /** 450 | * @param callable(Throwable): TrySafe $callable 451 | * 452 | * @return self 453 | */ 454 | abstract public function recoverWith(callable $callable): self; 455 | 456 | /** 457 | * Runs recovery only if predicate returns true. E.g. to check type of Throwable 458 | * 459 | * Recovery must return TrySafe::success() if the recovery is successful or TrySafe::failure() if it fails. 460 | * That allows chaining multiple calls with possible failure. 461 | * 462 | * @param callable(Throwable): bool $predicate - checks if conditions are met to run recovery. 463 | * @param callable(Throwable): TrySafe $recovery - a recovery you want to try run 464 | * 465 | * @return self 466 | */ 467 | abstract public function recoverWithIf(callable $predicate, callable $recovery): self; 468 | 469 | abstract public function isSuccess(): bool; 470 | 471 | final public function isFailure(): bool 472 | { 473 | return !$this->isSuccess(); 474 | } 475 | 476 | /** 477 | * Consider calling getOrElse instead 478 | * 479 | * @throws ValueIsNotPresentException 480 | * 481 | * @return T 482 | */ 483 | abstract public function getUnsafe(); 484 | 485 | /** 486 | * @template E 487 | * 488 | * @param E $else 489 | * 490 | * @return T|E 491 | */ 492 | abstract public function getOrElse($else); 493 | 494 | /** @throws ValueIsNotPresentException */ 495 | abstract public function getFailureUnsafe(): Throwable; 496 | 497 | /** @return Option */ 498 | abstract public function toOption(): Option; 499 | 500 | /** @return Either */ 501 | abstract public function toEither(): Either; 502 | 503 | /** 504 | * @template B 505 | * 506 | * @param callable(Throwable): B $handleFailure 507 | * @param callable(T): B $handleSuccess 508 | * 509 | * @return B 510 | */ 511 | abstract public function resolve(callable $handleFailure, callable $handleSuccess); 512 | } 513 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/EitherTest.php: -------------------------------------------------------------------------------- 1 | isLeft()); 17 | 18 | $right = Either::right(666); 19 | self::assertTrue($right->isRight()); 20 | 21 | $pure = Either::of(666); 22 | self::assertTrue($pure->isRight()); 23 | } 24 | 25 | public function testLift(): void 26 | { 27 | 28 | $left = Either::left('error'); 29 | $one = Either::right(1); 30 | $four = Either::right(4); 31 | 32 | $plus = static fn (int $x, int $y): int => $x + $y; 33 | 34 | $this->equals( 35 | Either::right(5), 36 | Either::lift($plus)($one, $four), 37 | ); 38 | $this->equals( 39 | $left, 40 | Either::lift($plus)($one, $left), 41 | ); 42 | } 43 | 44 | public function testLiftN(): void 45 | { 46 | self::assertEquals(Either::right(42), Either::lift1(static fn (int $a): int => $a)(Either::right(42))); 47 | self::assertEquals( 48 | Either::right(708), 49 | Either::lift2(static fn (int $a, int $b): int => $a + $b)(Either::right(42), Either::right(666)), 50 | ); 51 | self::assertEquals( 52 | Either::left("fail"), 53 | Either::lift2(static fn (int $a, int $b): int => $a + $b)(Either::right(42), Either::left("fail")), 54 | ); 55 | } 56 | 57 | public function testMap(): void 58 | { 59 | $greeter = static fn (string $s): string => sprintf('Hello %s', $s); 60 | 61 | $this->equals( 62 | Either::right('Hello world'), 63 | Either::right('world')->map($greeter), 64 | ); 65 | $this->equals( 66 | Either::left('error'), 67 | Either::left('error')->map($greeter), 68 | ); 69 | } 70 | 71 | public function testMapLeft(): void 72 | { 73 | $greeter = static fn (string $s): string => sprintf('Hello %s', $s); 74 | 75 | $this->equals( 76 | Either::right('world'), 77 | Either::right('world')->mapLeft($greeter), 78 | ); 79 | $this->equals( 80 | Either::left('Hello error'), 81 | Either::left('error')->mapLeft($greeter), 82 | ); 83 | } 84 | 85 | public function testFlatMap(): void 86 | { 87 | $politeGreeter = static fn (string $s): Either => Either::right(sprintf('Hello %s', $s)); 88 | 89 | $failingGreeter = static fn (string $s): Either => Either::left('No manners'); 90 | 91 | $this->equals( 92 | Either::right('Hello world'), 93 | Either::right('world')->flatMap($politeGreeter), 94 | ); 95 | $this->equals( 96 | Either::left('No manners'), 97 | Either::right('world')->flatMap($failingGreeter), 98 | ); 99 | $this->equals( 100 | Either::left('error'), 101 | Either::left('error')->flatMap($politeGreeter), 102 | ); 103 | } 104 | 105 | public function testFlatMapLeft(): void 106 | { 107 | $this->equals( 108 | Either::right('world'), 109 | Either::right('world')->flatMapLeft(static fn ($s) => Either::right("will not be called")), 110 | ); 111 | $this->equals( 112 | Either::left('Fail'), 113 | Either::left('error')->flatMapLeft(static fn ($s): Either => Either::left('Fail')), 114 | ); 115 | $this->equals( 116 | Either::right('Hello error'), 117 | Either::left('error')->flatMapLeft(static fn ($s): Either => Either::right(sprintf('Hello %s', $s))), 118 | ); 119 | } 120 | 121 | public function testExists(): void 122 | { 123 | $right = Either::right('Hello world'); 124 | $left = Either::left('error'); 125 | 126 | $falsyPredicate = static fn (): bool => false; 127 | 128 | $this->equals( 129 | true, 130 | $right->exists(tautology()), 131 | ); 132 | $this->equals( 133 | false, 134 | $left->exists(tautology()), 135 | ); 136 | $this->equals( 137 | false, 138 | $right->exists($falsyPredicate), 139 | ); 140 | $this->equals( 141 | false, 142 | $left->exists($falsyPredicate), 143 | ); 144 | } 145 | 146 | public function testGetRightUnsafe(): void 147 | { 148 | $val = 'Hello world'; 149 | $right = Either::right($val); 150 | 151 | self::assertEquals($val, $right->getRightUnsafe()); 152 | try { 153 | Either::left('error')->getRightUnsafe(); 154 | self::fail('Calling get method or None must throw'); 155 | } catch (Throwable $e) { 156 | self::assertInstanceOf(ValueIsNotPresentException::class, $e); 157 | } 158 | } 159 | 160 | public function testGetLeftUnsafe(): void 161 | { 162 | $val = 'error'; 163 | $left = Either::left($val); 164 | 165 | self::assertEquals($val, $left->getLeftUnsafe()); 166 | try { 167 | Either::right('Hello world')->getLeftUnsafe(); 168 | self::fail('Calling get method or None must throw'); 169 | } catch (Throwable $e) { 170 | self::assertInstanceOf(ValueIsNotPresentException::class, $e); 171 | } 172 | } 173 | 174 | public function testGetOrElse(): void 175 | { 176 | $val = 'Hello world'; 177 | $some = Either::right($val); 178 | 179 | $else = 'Embrace the dark lord'; 180 | self::assertEquals($val, $some->getOrElse($else)); 181 | self::assertEquals($else, Either::left(666)->getOrElse($else)); 182 | } 183 | 184 | public function testToTrySafe(): void 185 | { 186 | $val = 'Hello world'; 187 | self::assertEquals($val, Either::right($val)->toTrySafe()->getUnsafe()); 188 | self::assertInstanceOf(ValueIsNotPresentException::class, Either::left(666)->toTrySafe()->getFailureUnsafe()); 189 | } 190 | 191 | public function testIterator(): void 192 | { 193 | $val = 'Hello world'; 194 | $some = Either::right($val); 195 | 196 | self::assertEquals([$val], iterator_to_array($some->getIterator(), false)); 197 | self::assertEquals([], iterator_to_array(Either::left(666)->getIterator(), false)); 198 | } 199 | 200 | public function testReduce(): void 201 | { 202 | $sum = static fn (int $a, int $b): int => $a + $b; 203 | $init = 4; 204 | 205 | self::assertEquals( 206 | 42, 207 | Either::right(38)->reduce($sum, $init), 208 | ); 209 | self::assertEquals( 210 | $init, 211 | Either::left(666)->reduce($sum, $init), 212 | ); 213 | } 214 | 215 | public function testEach(): void 216 | { 217 | $accumulator = 0; 218 | $accumulate = static function (int $i) use (&$accumulator): void { 219 | $accumulator += $i; 220 | }; 221 | 222 | Either::left(666)->each($accumulate); 223 | self::assertEquals(0, $accumulator); 224 | 225 | Either::right(42)->each($accumulate); 226 | self::assertEquals(42, $accumulator); 227 | } 228 | 229 | public function testTap(): void 230 | { 231 | $right = Either::right(42); 232 | $left = Either::left(666); 233 | $accumulated = 0; 234 | 235 | $accumulate = static function (int $i) use (&$accumulated): void { 236 | $accumulated += $i; 237 | }; 238 | 239 | self::assertSame($left, $left->tap($accumulate)); 240 | self::assertSame(666, $left->getLeftUnsafe()); 241 | self::assertEquals(0, $accumulated); 242 | 243 | self::assertSame($right, $right->tap($accumulate)); 244 | self::assertSame(42, $right->getRightUnsafe()); 245 | self::assertEquals(42, $accumulated); 246 | } 247 | 248 | public function testTapLeft(): void 249 | { 250 | $right = Either::right(42); 251 | $left = Either::left(666); 252 | $accumulated = 0; 253 | 254 | $accumulate = static function (int $i) use (&$accumulated): void { 255 | $accumulated += $i; 256 | }; 257 | 258 | self::assertSame($left, $left->tapLeft($accumulate)); 259 | self::assertSame(666, $left->getLeftUnsafe()); 260 | self::assertEquals(666, $accumulated); 261 | 262 | self::assertSame($right, $right->tapLeft($accumulate)); 263 | self::assertSame(42, $right->getRightUnsafe()); 264 | self::assertEquals(666, $accumulated); 265 | } 266 | 267 | public function testAp(): void 268 | { 269 | $plus = CurriedFunction::curry2(static fn (int $x, int $y): int => $x + $y); 270 | /** @var Either>> */ 271 | $leftOp = Either::left('undefined operation'); 272 | 273 | $purePlus = Either::of($plus); 274 | /** @var Either */ 275 | $left = Either::left('missing value'); 276 | /** @var Either */ 277 | $one = Either::right(1); 278 | /** @var Either */ 279 | $two = Either::right(2); 280 | /** @var Either */ 281 | $three = Either::right(3); 282 | 283 | $this->equals(Either::ap(Either::ap($purePlus, $one), $two), $three); 284 | $this->equals(Either::ap(Either::ap($purePlus, $one), $left), $left); 285 | $this->equals(Either::ap(Either::ap($purePlus, $left), $one), $left); 286 | $this->equals(Either::ap(Either::ap($purePlus, $left), $left), $left); 287 | $this->equals(Either::ap(Either::ap($leftOp, $one), $two), $leftOp); 288 | } 289 | 290 | public function testProduct(): void 291 | { 292 | self::assertEquals(Either::right([1, 'a']), Either::product(Either::right(1), Either::right('a'))); 293 | self::assertEquals(Either::left('fuu'), Either::product(Either::right(1), Either::left('fuu'))); 294 | } 295 | 296 | public function testTraverse(): void 297 | { 298 | $iterable = [ 299 | Either::right(42), 300 | Either::right(666), 301 | ]; 302 | 303 | $iterableWithFails = [ 304 | Either::left('error1'), 305 | Either::left('error2'), 306 | Either::right(42), 307 | ]; 308 | 309 | /** @phpstan-var array> $emptyIterable */ 310 | $emptyIterable = []; 311 | 312 | self::assertEquals([42, 666], Either::sequence($iterable)->getRightUnsafe()->toArray()); 313 | self::assertEquals([], Either::sequence($emptyIterable)->getRightUnsafe()->toArray()); 314 | self::assertEquals(Either::left('error1'), Either::sequence($iterableWithFails)); 315 | 316 | $numbersLowerThan10 = [1, 2, 3, 7, 9]; 317 | 318 | $wrapLowerThan10 = static fn (int $int): Either => $int < 10 319 | ? Either::right($int) 320 | : Either::left('higher then ten'); 321 | 322 | $wrapLowerThan9 = static fn (int $int): Either => $int < 9 323 | ? Either::right($int) 324 | : Either::left('higher then nine'); 325 | 326 | self::assertEquals( 327 | $numbersLowerThan10, 328 | Either::traverse($numbersLowerThan10, $wrapLowerThan10)->getRightUnsafe()->toArray(), 329 | ); 330 | self::assertEquals( 331 | Either::left('higher then nine'), 332 | Either::traverse($numbersLowerThan10, $wrapLowerThan9), 333 | ); 334 | } 335 | 336 | public function testSequence(): void 337 | { 338 | $iterable = [ 339 | Either::right(42), 340 | Either::right(666), 341 | ]; 342 | 343 | $iterableWithFails = [ 344 | Either::right(42), 345 | Either::left('error2'), 346 | Either::left('error1'), 347 | ]; 348 | 349 | /** @phpstan-var array> $emptyIterable */ 350 | $emptyIterable = []; 351 | 352 | self::assertEquals([42, 666], Either::sequence($iterable)->getRightUnsafe()->toArray()); 353 | self::assertEquals([], Either::sequence($emptyIterable)->getRightUnsafe()->toArray()); 354 | self::assertEquals(Either::left('error2'), Either::sequence($iterableWithFails)); 355 | } 356 | 357 | public function testOrElse(): void 358 | { 359 | $right42 = Either::right(5); 360 | $right666 = Either::right(666); 361 | $error = Either::left('error'); 362 | 363 | $this->equals($right42, $right42->orElse($right666)); 364 | $this->equals($right666, $error->orElse($right666)); 365 | } 366 | 367 | public function testResolveRight(): void 368 | { 369 | $handleRightSpy = createInvokableSpy(); 370 | $handleLeftSpy = createInvokableSpy(); 371 | 372 | Either::right(42) 373 | ->resolve($handleLeftSpy, $handleRightSpy); 374 | 375 | self::assertCount(1, $handleRightSpy->getCalls()); 376 | self::assertCount(0, $handleLeftSpy->getCalls()); 377 | self::assertEquals([[42]], $handleRightSpy->getCalls()); 378 | } 379 | 380 | public function testResolveLeft(): void 381 | { 382 | $handleRightSpy = createInvokableSpy(); 383 | $handleLeftSpy = createInvokableSpy(); 384 | 385 | Either::left(666) 386 | ->resolve($handleLeftSpy, $handleRightSpy); 387 | 388 | self::assertCount(0, $handleRightSpy->getCalls()); 389 | self::assertCount(1, $handleLeftSpy->getCalls()); 390 | self::assertSame([[666]], $handleLeftSpy->getCalls()); 391 | } 392 | 393 | public function testLaws(): void 394 | { 395 | $assertEquals = function ($a, $b): void { 396 | $this->equals($a, $b); 397 | }; 398 | $eitherEquals = static fn (Either $a, Either $b): bool => $a->equals($b); 399 | $ap = Either::ap(...); 400 | $pure = Either::pure(...); 401 | 402 | $rightOne = Either::right(1); 403 | $rightTwo = Either::right(2); 404 | $rightThree = Either::right(3); 405 | $error = Either::left('error'); 406 | 407 | $plus2 = CurriedFunction::of(static fn (int $x): int => $x + 2); 408 | $multiple2 = CurriedFunction::of(static fn (int $x): int => $x * 2); 409 | 410 | testEqualsReflexivity($assertEquals, $eitherEquals, $rightOne); 411 | testEqualsReflexivity($assertEquals, $eitherEquals, $error); 412 | 413 | testEqualsSymmetry($assertEquals, $eitherEquals, $rightOne, $rightOne); 414 | testEqualsSymmetry($assertEquals, $eitherEquals, $rightOne, $rightTwo); 415 | testEqualsSymmetry($assertEquals, $eitherEquals, $rightOne, $error); 416 | testEqualsSymmetry($assertEquals, $eitherEquals, $error, $error); 417 | 418 | testEqualsTransitivity($assertEquals, $eitherEquals, $rightOne, $rightOne, $rightOne); 419 | testEqualsTransitivity($assertEquals, $eitherEquals, $rightOne, $rightTwo, $rightThree); 420 | testEqualsTransitivity($assertEquals, $eitherEquals, $rightOne, $error, $rightThree); 421 | 422 | testFunctorIdentity($assertEquals, $rightOne); 423 | testFunctorIdentity($assertEquals, $error); 424 | 425 | testFunctorComposition($assertEquals, $rightOne, $plus2, $multiple2); 426 | testFunctorComposition($assertEquals, $error, $plus2, $multiple2); 427 | 428 | testApplicativeIdentity($assertEquals, $ap, $pure, $rightOne); 429 | testApplicativeIdentity($assertEquals, $ap, $pure, $error); 430 | 431 | testApplicativeHomomorphism($assertEquals, $ap, $pure, 666, $multiple2); 432 | testApplicativeHomomorphism($assertEquals, $ap, $pure, 666, $multiple2); 433 | 434 | testApplicativeComposition($assertEquals, $ap, $pure, $rightOne, $pure($plus2), $pure($multiple2)); 435 | testApplicativeComposition($assertEquals, $ap, $pure, $error, $pure($plus2), $pure($multiple2)); 436 | testApplicativeComposition($assertEquals, $ap, $pure, $rightOne, $error, $pure($multiple2)); 437 | testApplicativeComposition($assertEquals, $ap, $pure, $error, $pure($plus2), $error); 438 | 439 | testApplicativeInterchange($assertEquals, $ap, $pure, 666, $pure($plus2)); 440 | testApplicativeInterchange($assertEquals, $ap, $pure, 666, $error); 441 | } 442 | 443 | /** 444 | * @template A 445 | * @template B 446 | * 447 | * @phpstan-param A $a 448 | * @phpstan-param B $b 449 | * 450 | * @phpstan-return void 451 | */ 452 | private function equals($a, $b): void 453 | { 454 | if ($a instanceof Either && $b instanceof Either) { 455 | self::assertTrue($a->equals($b)); 456 | } else { 457 | self::assertEquals($a, $b); 458 | } 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/OptionTest.php: -------------------------------------------------------------------------------- 1 | isDefined()); 18 | self::assertTrue($none->isEmpty()); 19 | 20 | $some = Option::some(666); 21 | self::assertTrue($some->isDefined()); 22 | self::assertFalse($some->isEmpty()); 23 | 24 | $fromNull = Option::of(null); 25 | self::assertTrue($fromNull->isDefined()); 26 | self::assertFalse($fromNull->isEmpty()); 27 | } 28 | 29 | public function testCreateFromNullable(): void 30 | { 31 | self::assertTrue(Option::fromNullable('Look, I exist')->isDefined()); 32 | self::assertFalse(Option::fromNullable(null)->isDefined()); 33 | } 34 | 35 | public function testLift(): void 36 | { 37 | $none = Option::none(); 38 | $xOpt = Option::some(1); 39 | $yOpt = Option::some(4); 40 | 41 | $plus = static fn (int $x, int $y): int => $x + $y; 42 | 43 | $this->equals( 44 | Option::some(5), 45 | Option::lift($plus)($xOpt, $yOpt), 46 | ); 47 | $this->equals( 48 | $none, 49 | Option::lift($plus)($xOpt, $none), 50 | ); 51 | } 52 | 53 | public function testLiftN(): void 54 | { 55 | self::assertEquals(Option::some(42), Option::lift1(static fn (int $a): int => $a)(Option::some(42))); 56 | self::assertEquals( 57 | Option::some(708), 58 | Option::lift2(static fn (int $a, int $b): int => $a + $b)(Option::some(42), Option::some(666)), 59 | ); 60 | } 61 | 62 | public function testMap(): void 63 | { 64 | $mapper = static fn (string $s): string => sprintf('Hello %s', $s); 65 | $mapperToNull = static fn (string $s) => null; 66 | 67 | $this->equals( 68 | Option::some('Hello world'), 69 | Option::some('world')->map($mapper), 70 | ); 71 | $this->equals( 72 | Option::none(), 73 | Option::none()->map($mapper), 74 | ); 75 | 76 | $this->equals( 77 | Option::some(null), 78 | Option::some('world')->map($mapperToNull), 79 | ); 80 | $this->equals( 81 | Option::none(), 82 | Option::none()->map($mapperToNull), 83 | ); 84 | } 85 | 86 | public function testFlatMap(): void 87 | { 88 | $mapperToSome = static fn (string $s): Option => Option::some(sprintf('Hello %s', $s)); 89 | 90 | $mapperToNone = static fn (string $s): Option => Option::none(); 91 | 92 | $this->equals( 93 | Option::some('Hello world'), 94 | Option::some('world')->flatMap($mapperToSome), 95 | ); 96 | $this->equals( 97 | Option::none(), 98 | Option::none()->flatMap($mapperToSome), 99 | ); 100 | $this->equals( 101 | Option::none(), 102 | Option::some('world')->flatMap($mapperToNone), 103 | ); 104 | $this->equals( 105 | Option::none(), 106 | Option::none()->flatMap($mapperToNone), 107 | ); 108 | } 109 | 110 | public function testFilter(): void 111 | { 112 | $some = Option::some('Hello world'); 113 | 114 | $falsyPredicate = static fn (): bool => false; 115 | 116 | $this->equals( 117 | $some, 118 | $some->filter(tautology()), 119 | ); 120 | $this->equals( 121 | Option::none(), 122 | Option::none()->filter(tautology()), 123 | ); 124 | $this->equals( 125 | Option::none(), 126 | $some->filter($falsyPredicate), 127 | ); 128 | $this->equals( 129 | Option::none(), 130 | Option::none()->filter($falsyPredicate), 131 | ); 132 | } 133 | 134 | public function testExists(): void 135 | { 136 | $some = Option::some('Hello world'); 137 | 138 | $falsyPredicate = static fn (): bool => false; 139 | 140 | $this->equals( 141 | true, 142 | $some->exists(tautology()), 143 | ); 144 | $this->equals( 145 | false, 146 | Option::none()->exists(tautology()), 147 | ); 148 | $this->equals( 149 | false, 150 | $some->exists($falsyPredicate), 151 | ); 152 | $this->equals( 153 | false, 154 | Option::none()->exists($falsyPredicate), 155 | ); 156 | } 157 | 158 | public function testGetUnsafe(): void 159 | { 160 | $val = 'Hello world'; 161 | $some = Option::some($val); 162 | 163 | self::assertEquals($val, $some->getUnsafe()); 164 | try { 165 | Option::none()->getUnsafe(); 166 | self::fail('Calling get method or None must throw'); 167 | } catch (Throwable $e) { 168 | self::assertInstanceOf(ValueIsNotPresentException::class, $e); 169 | } 170 | } 171 | 172 | public function testGetOrElse(): void 173 | { 174 | $val = 'Hello world'; 175 | $some = Option::some($val); 176 | 177 | $else = 'Embrace the dark lord'; 178 | self::assertEquals($val, $some->getOrElse($else)); 179 | self::assertEquals($else, Option::none()->getOrElse($else)); 180 | } 181 | 182 | public function testGetOrElseLazy(): void 183 | { 184 | $val = 'Hello world'; 185 | $some = Option::some($val); 186 | 187 | $else = 'Embrace the dark lord'; 188 | self::assertEquals($val, $some->getOrElseLazy(static fn () => $else)); 189 | self::assertEquals($else, Option::none()->getOrElseLazy(static fn () => $else)); 190 | } 191 | 192 | public function testGetOrThrow(): void 193 | { 194 | $val = 'Hello world'; 195 | $some = Option::some($val); 196 | 197 | $exception = new RuntimeException('Embrace the dark lord'); 198 | $throw = static function () use ($exception) { 199 | throw $exception; 200 | }; 201 | self::assertEquals($val, $some->getOrThrow($throw)); 202 | try { 203 | Option::none()->getOrThrow($throw); 204 | self::fail('Calling getOrElseThrow method or None must throw'); 205 | } catch (Throwable $e) { 206 | self::assertInstanceOf(RuntimeException::class, $e); 207 | self::assertEquals('Embrace the dark lord', $e->getMessage()); 208 | } 209 | } 210 | 211 | public function testToTrySafe(): void 212 | { 213 | $val = 'Hello world'; 214 | self::assertEquals($val, Option::some($val)->toTrySafe()->getUnsafe()); 215 | self::assertInstanceOf(ValueIsNotPresentException::class, Option::none()->toTrySafe()->getFailureUnsafe()); 216 | } 217 | 218 | public function testToEither(): void 219 | { 220 | $val = 'Hello world'; 221 | self::assertEquals($val, Option::some($val)->toEither(42)->getRightUnsafe()); 222 | self::assertEquals(42, Option::none()->toEither(42)->getLeftUnsafe()); 223 | } 224 | 225 | public function testToList(): void 226 | { 227 | $val = 'Hello world'; 228 | self::assertEquals([$val], Option::some($val)->toList()->toArray()); 229 | self::assertEquals([], Option::none()->toList()->toArray()); 230 | } 231 | 232 | public function testToArray(): void 233 | { 234 | $val = 'Hello world'; 235 | self::assertEquals([$val], Option::some($val)->toArray()); 236 | self::assertEquals([], Option::none()->toArray()); 237 | } 238 | 239 | public function testIterator(): void 240 | { 241 | $val = 'Hello world'; 242 | $some = Option::some($val); 243 | 244 | self::assertEquals([$val], iterator_to_array($some->getIterator(), false)); 245 | self::assertEquals([], iterator_to_array(Option::none()->getIterator(), false)); 246 | } 247 | 248 | public function testReduce(): void 249 | { 250 | $reducer = static fn (int $reduction, int $val): int => $reduction + $val; 251 | $initialReduction = 4; 252 | 253 | self::assertEquals( 254 | 670, 255 | Option::some(666)->reduce($reducer, $initialReduction), 256 | ); 257 | self::assertEquals( 258 | $initialReduction, 259 | Option::none()->reduce($reducer, $initialReduction), 260 | ); 261 | } 262 | 263 | public function testEach(): void 264 | { 265 | $accumulator = 0; 266 | $accumulate = static function (int $i) use (&$accumulator): void { 267 | $accumulator += $i; 268 | }; 269 | 270 | Option::none()->each($accumulate); 271 | self::assertEquals(0, $accumulator); 272 | 273 | Option::some(5)->each($accumulate); 274 | self::assertEquals(5, $accumulator); 275 | } 276 | 277 | public function testTap(): void 278 | { 279 | $some = Option::some(1); 280 | $none = Option::none(); 281 | $accumulated = 0; 282 | 283 | $accumulate = static function (int $i) use (&$accumulated): void { 284 | $accumulated += $i; 285 | }; 286 | 287 | self::assertSame($none, $none->tap($accumulate)); 288 | self::assertEquals(0, $accumulated); 289 | 290 | self::assertSame($some, $some->tap($accumulate)); 291 | self::assertEquals(1, $accumulated); 292 | } 293 | 294 | public function testTapNone(): void 295 | { 296 | $some = Option::some(1); 297 | $none = Option::none(); 298 | $counter = 0; 299 | 300 | $increment = static function () use (&$counter): void { 301 | $counter++; 302 | }; 303 | 304 | self::assertSame($some, $some->tapNone($increment)); 305 | self::assertEquals(0, $counter); 306 | 307 | self::assertSame($none, $none->tapNone($increment)); 308 | self::assertEquals(1, $counter); 309 | } 310 | 311 | public function testAp(): void 312 | { 313 | $purePlus = Option::of(CurriedFunction::curry2(static fn (int $x, int $y): int => $x + $y)); 314 | /** @var Option $noneInt */ 315 | $noneInt = Option::none(); 316 | $one = Option::some(1); 317 | $two = Option::some(2); 318 | $three = Option::some(3); 319 | 320 | /** @var Option>> $noneClosure */ 321 | $noneClosure = Option::none(); 322 | 323 | $this->equals(Option::ap(Option::ap($purePlus, $one), $two), $three); 324 | $this->equals(Option::ap(Option::ap($purePlus, $one), $noneInt), $noneInt); 325 | $this->equals(Option::ap(Option::ap($purePlus, $noneInt), $one), $noneInt); 326 | $this->equals(Option::ap(Option::ap($purePlus, $noneInt), $noneInt), $noneInt); 327 | $this->equals(Option::ap(Option::ap($noneClosure, $one), $two), $noneInt); 328 | } 329 | 330 | public function testProduct(): void 331 | { 332 | self::assertEquals(Option::some([1, 'a']), Option::product(Option::some(1), Option::some('a'))); 333 | self::assertEquals(Option::none(), Option::product(Option::some(1), Option::none())); 334 | } 335 | 336 | public function testTraverse(): void 337 | { 338 | $iterable = [ 339 | Option::some(42), 340 | Option::some(666), 341 | ]; 342 | 343 | $iterableWithNone = [ 344 | Option::some(42), 345 | Option::none(), 346 | ]; 347 | 348 | /** @phpstan-var array> $emptyIterable */ 349 | $emptyIterable = []; 350 | 351 | self::assertEquals([42, 666], Option::sequence($iterable)->getUnsafe()->toArray()); 352 | self::assertEquals([], Option::sequence($emptyIterable)->getUnsafe()->toArray()); 353 | self::assertSame(Option::none(), Option::sequence($iterableWithNone)); 354 | 355 | $numbersLowerThan10 = [1, 2, 3, 7, 9]; 356 | 357 | $wrapLowerThan10 = static fn (int $int): Option => $int < 10 358 | ? Option::some($int) 359 | : Option::none(); 360 | 361 | $wrapLowerThan9 = static fn (int $int): Option => $int < 9 362 | ? Option::some($int) 363 | : Option::none(); 364 | 365 | self::assertEquals( 366 | $numbersLowerThan10, 367 | Option::traverse($numbersLowerThan10, $wrapLowerThan10)->getUnsafe()->toArray(), 368 | ); 369 | self::assertSame( 370 | Option::none(), 371 | Option::traverse($numbersLowerThan10, $wrapLowerThan9), 372 | ); 373 | } 374 | 375 | public function testSequence(): void 376 | { 377 | $iterable = [ 378 | Option::some(42), 379 | Option::some(666), 380 | ]; 381 | 382 | $iterableWithNone = [ 383 | Option::some(42), 384 | Option::none(), 385 | ]; 386 | 387 | /** @phpstan-var array> $emptyIterable */ 388 | $emptyIterable = []; 389 | 390 | self::assertEquals([42, 666], Option::sequence($iterable)->getUnsafe()->toArray()); 391 | self::assertEquals([], Option::sequence($emptyIterable)->getUnsafe()->toArray()); 392 | self::assertSame(Option::none(), Option::sequence($iterableWithNone)); 393 | } 394 | 395 | public function testOrElse(): void 396 | { 397 | $some42 = Option::some(5); 398 | $some666 = Option::some(666); 399 | $none = Option::none(); 400 | 401 | $this->equals($some42, $some42->orElse($some666)); 402 | $this->equals($some666, $none->orElse($some666)); 403 | } 404 | 405 | public function testResolveSome(): void 406 | { 407 | $handleSomeSpy = createInvokableSpy(); 408 | $handleNoneSpy = createInvokableSpy(); 409 | 410 | Option::some(666) 411 | ->resolve($handleNoneSpy, $handleSomeSpy); 412 | 413 | self::assertCount(1, $handleSomeSpy->getCalls()); 414 | self::assertCount(0, $handleNoneSpy->getCalls()); 415 | self::assertEquals([[666]], $handleSomeSpy->getCalls()); 416 | } 417 | 418 | public function testResolveNone(): void 419 | { 420 | $handleSomeSpy = createInvokableSpy(); 421 | $handleNoneSpy = createInvokableSpy(); 422 | 423 | Option::none() 424 | ->resolve($handleNoneSpy, $handleSomeSpy); 425 | 426 | self::assertCount(0, $handleSomeSpy->getCalls()); 427 | self::assertCount(1, $handleNoneSpy->getCalls()); 428 | self::assertSame([[]], $handleNoneSpy->getCalls()); 429 | } 430 | 431 | public function testLaws(): void 432 | { 433 | 434 | $assertEquals = function ($a, $b): void { 435 | $this->equals($a, $b); 436 | }; 437 | $optionEquals = static fn (Option $a, Option $b): bool => $a->equals($b); 438 | $ap = Option::ap(...); 439 | $pure = Option::pure(...); 440 | 441 | $someOne = Option::some(1); 442 | $someTwo = Option::some(2); 443 | $someThree = Option::some(3); 444 | $none = Option::none(); 445 | 446 | $plus2 = CurriedFunction::of(static fn (int $x): int => $x + 2); 447 | $multiple2 = CurriedFunction::of(static fn (int $x): int => $x * 2); 448 | 449 | testEqualsReflexivity($assertEquals, $optionEquals, $someOne); 450 | testEqualsReflexivity($assertEquals, $optionEquals, $none); 451 | 452 | testEqualsSymmetry($assertEquals, $optionEquals, $someOne, $someOne); 453 | testEqualsSymmetry($assertEquals, $optionEquals, $someOne, $someTwo); 454 | testEqualsSymmetry($assertEquals, $optionEquals, $someOne, $none); 455 | testEqualsSymmetry($assertEquals, $optionEquals, $none, $none); 456 | 457 | testEqualsTransitivity($assertEquals, $optionEquals, $someOne, $someOne, $someOne); 458 | testEqualsTransitivity($assertEquals, $optionEquals, $someOne, $someTwo, $someThree); 459 | testEqualsTransitivity($assertEquals, $optionEquals, $someOne, $none, $someThree); 460 | 461 | testFunctorIdentity($assertEquals, $someOne); 462 | testFunctorIdentity($assertEquals, $none); 463 | 464 | testFunctorComposition($assertEquals, $someOne, $plus2, $multiple2); 465 | testFunctorComposition($assertEquals, $none, $plus2, $multiple2); 466 | 467 | testApplicativeIdentity($assertEquals, $ap, $pure, $someOne); 468 | testApplicativeIdentity($assertEquals, $ap, $pure, $none); 469 | 470 | testApplicativeHomomorphism($assertEquals, $ap, $pure, 666, $multiple2); 471 | testApplicativeHomomorphism($assertEquals, $ap, $pure, 666, $multiple2); 472 | 473 | testApplicativeComposition($assertEquals, $ap, $pure, $someOne, $pure($plus2), $pure($multiple2)); 474 | testApplicativeComposition($assertEquals, $ap, $pure, $none, $pure($plus2), $pure($multiple2)); 475 | testApplicativeComposition($assertEquals, $ap, $pure, $someOne, $none, $pure($multiple2)); 476 | testApplicativeComposition($assertEquals, $ap, $pure, $none, $pure($plus2), $none); 477 | 478 | testApplicativeInterchange($assertEquals, $ap, $pure, 666, $pure($plus2)); 479 | testApplicativeInterchange($assertEquals, $ap, $pure, 666, $none); 480 | } 481 | 482 | /** 483 | * @template A 484 | * @template B 485 | * 486 | * @phpstan-param A $a 487 | * @phpstan-param B $b 488 | * 489 | * @phpstan-return void 490 | */ 491 | private function equals($a, $b): void 492 | { 493 | if ($a instanceof Option && $b instanceof Option) { 494 | self::assertTrue($a->equals($b)); 495 | } else { 496 | self::assertEquals($a, $b); 497 | } 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/LazyListTest.php: -------------------------------------------------------------------------------- 1 | toArray()); 20 | } 21 | 22 | public function testInfinityRange(): void 23 | { 24 | $range = LazyList::range(1) 25 | ->filter(static fn (int $number): bool => $number % 3 === 0) 26 | ->take(10) 27 | ->toArray(); 28 | self::assertEquals(range(3, 30, 3), $range); 29 | } 30 | 31 | public function testFill(): void 32 | { 33 | $fill = LazyList::fill('a', 2); 34 | self::assertEquals(['a', 'a'], $fill->toArray()); 35 | } 36 | 37 | public function testInfinityFill(): void 38 | { 39 | $filled = LazyList::fill('a') 40 | ->take(10) 41 | ->toArray(); 42 | self::assertEquals(array_fill(0, 10, 'a'), $filled); 43 | } 44 | 45 | public function testFromIterable(): void 46 | { 47 | $lazyList = LazyList::fromIterable(new ArrayIterator([1, 2])); 48 | self::assertEquals([1, 2], $lazyList->toArray()); 49 | } 50 | 51 | public function testFromItems(): void 52 | { 53 | $lazyList = LazyList::of(1, 2); 54 | self::assertEquals([1, 2], $lazyList->toArray()); 55 | } 56 | 57 | public function testMap(): void 58 | { 59 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 60 | $mapped = $lazyList->map(static fn ($item) => $item * 10); 61 | 62 | self::assertEquals(range(10, 100, 10), iterator_to_array($mapped)); 63 | } 64 | 65 | public function testMapWithKey(): void 66 | { 67 | $lazyList = new LazyList(new ArrayIterator(range(1, 10, 2))); 68 | $mapped = $lazyList->map(static fn ($item, $index) => $index); 69 | 70 | self::assertEquals(range(0, 4), iterator_to_array($mapped)); 71 | } 72 | 73 | public function testAp(): void 74 | { 75 | /** @var LazyList>>> */ 76 | $callbacks = LazyList::fromIterable([ 77 | CurriedFunction::curry2(static fn ($a, $b) => $a . $b), 78 | CurriedFunction::curry2(static fn ($a, $b) => [$a, $b]), 79 | ]); 80 | $numbersApplied = LazyList::ap($callbacks, LazyList::of('1', '2')); 81 | $lettersApplied = LazyList::ap($numbersApplied, LazyList::of('a', 'b')); 82 | 83 | $expected = [ 84 | '1a', 85 | '1b', 86 | '2a', 87 | '2b', 88 | ['1', 'a'], 89 | ['1', 'b'], 90 | ['2', 'a'], 91 | ['2', 'b'], 92 | ]; 93 | 94 | self::assertEquals($expected, $lettersApplied->toArray()); 95 | } 96 | 97 | public function testApNone(): void 98 | { 99 | /** @var LazyList>>> */ 100 | $callbacks = LazyList::fromIterable([ 101 | CurriedFunction::curry2(static fn ($a, $b) => $a . $b), 102 | CurriedFunction::curry2(static fn ($a, $b) => [$a, $b]), 103 | ]); 104 | /** @var LazyList $empty */ 105 | $empty = LazyList::fromEmpty(); 106 | $mapped = LazyList::ap(LazyList::ap($callbacks, LazyList::of('1', '2')), $empty); 107 | 108 | self::assertEquals([], $mapped->toArray()); 109 | } 110 | 111 | public function testLift(): void 112 | { 113 | $lifted = LazyList::lift(static fn ($a, $b) => $a . $b); 114 | 115 | $mapped = $lifted(LazyList::of(1, 2), LazyList::of('a', 'b')); 116 | 117 | self::assertEquals(['1a', '1b', '2a', '2b'], $mapped->toArray()); 118 | } 119 | 120 | public function testSequence(): void 121 | { 122 | self::assertEquals( 123 | [ArrayList::of(1, 2)], 124 | LazyList::sequence([LazyList::of(1), LazyList::of(2)])->toArray(), 125 | ); 126 | self::assertEquals( 127 | [], 128 | LazyList::sequence([LazyList::of(1), LazyList::fromEmpty()])->toArray(), 129 | ); 130 | /** @phpstan-var array> $empty */ 131 | $empty = []; 132 | self::assertEquals( 133 | [ArrayList::fromEmpty()], 134 | LazyList::sequence($empty)->toArray(), 135 | ); 136 | } 137 | 138 | public function testSequenceWithMultipleValues(): void 139 | { 140 | /** @var array> $iterable */ 141 | $iterable = [LazyList::of(1, 2), LazyList::of('a', 'b')]; 142 | self::assertEquals( 143 | [ 144 | ArrayList::fromIterable([1, 'a']), 145 | ArrayList::fromIterable([1, 'b']), 146 | ArrayList::fromIterable([2, 'a']), 147 | ArrayList::fromIterable([2, 'b']), 148 | ], 149 | LazyList::sequence($iterable)->toArray(), 150 | ); 151 | } 152 | 153 | public function testFlatMap(): void 154 | { 155 | $lazyList = new LazyList([1, 2, 3]); 156 | $mapped = $lazyList->flatMap(static fn (int $item): array => [$item, [$item * 2]]); 157 | self::assertEquals([1, [2], 2, [4], 3, [6]], $mapped->toArray()); 158 | } 159 | 160 | public function testFlatten(): void 161 | { 162 | $lazyList = new LazyList([[[1], [2]], [[3]]]); 163 | self::assertEquals([[1], [2], [3]], $lazyList->flatten()->toArray()); 164 | } 165 | 166 | public function testItShouldFailWhenItemCannotBeFlattened(): void 167 | { 168 | $lazyList = new LazyList([[[1], [2]], 3]); 169 | $this->expectException(RuntimeException::class); 170 | $this->expectExceptionMessage('Some item cannot be flattened because it is not iterable'); 171 | $lazyList->flatten()->toArray(); 172 | } 173 | 174 | public function testEach(): void 175 | { 176 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 177 | 178 | $accumulator = 0; 179 | $lazyList->each(static function (int $item) use (&$accumulator): void { 180 | $accumulator += $item; 181 | }); 182 | 183 | self::assertEquals(55, $accumulator); 184 | } 185 | 186 | public function testTap(): void 187 | { 188 | $lazyList = LazyList::range(1, 3); 189 | 190 | $accumulated = 0; 191 | $acumulate = static function (int $item) use (&$accumulated): void { 192 | $accumulated += $item; 193 | }; 194 | 195 | $concatenated = ''; 196 | $concat = static function (string $string) use (&$concatenated): void { 197 | $concatenated .= $string; 198 | }; 199 | 200 | $materialized = $lazyList 201 | ->tap($acumulate) 202 | ->map(static fn (int $x): string => (string)$x) 203 | ->tap($concat) 204 | ->toArray(); 205 | 206 | self::assertSame(['1', '2', '3'], $materialized); 207 | self::assertSame(6, $accumulated); 208 | self::assertSame('123', $concatenated); 209 | } 210 | 211 | public function testReduce(): void 212 | { 213 | $lazyList = new LazyList(range(1, 3)); 214 | 215 | $sum = $lazyList->reduce(static fn ($sum, $item) => $sum + $item, 0); 216 | self::assertEquals(6, $sum); 217 | } 218 | 219 | public function testMfold(): void 220 | { 221 | $list = LazyList::of(1, 2, 3); 222 | $sum = $list->mfold(new IntSumMonoid()); 223 | self::assertEquals(6, $sum); 224 | } 225 | 226 | public function testSum(): void 227 | { 228 | $list = LazyList::of((object)['a' => 1], (object)['a' => 2], (object)['a' => 3]); 229 | $sum = $list->sum(static fn (stdClass $o): int => $o->a); 230 | self::assertEquals(6, $sum); 231 | } 232 | 233 | public function testScan(): void 234 | { 235 | $lazyList = LazyList::fill(1); 236 | 237 | $sum = $lazyList->scan(static fn ($sum, $item) => $sum + $item, 0); 238 | self::assertEquals([1, 2, 3], $sum->take(3)->toArray()); 239 | } 240 | 241 | public function testMapRepeatedCall(): void 242 | { 243 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 244 | $mapped1 = $lazyList->map(static fn ($item) => $item * 10); 245 | $mapped2 = $lazyList->map(static fn ($item) => $item * 100); 246 | 247 | self::assertEquals(range(10, 100, 10), iterator_to_array($mapped1)); 248 | self::assertEquals(range(100, 1000, 100), iterator_to_array($mapped2)); 249 | } 250 | 251 | public function testDoWhile(): void 252 | { 253 | $taken = []; 254 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 255 | $lazyList 256 | ->tap(static function (int $i) use (&$taken): void { 257 | $taken[] = $i; 258 | }) 259 | ->doWhile(static fn (int $i): bool => $i <= 3); 260 | 261 | self::assertEquals(range(1, 4), $taken); 262 | } 263 | 264 | public function testRun(): void 265 | { 266 | $taken = []; 267 | $lazyList = LazyList::range(1, 3) 268 | ->tap(static function (int $i) use (&$taken): void { 269 | $taken[] = $i; 270 | }); 271 | 272 | self::assertEquals([], $taken); 273 | $lazyList->run(); 274 | self::assertEquals(range(1, 3), $taken); 275 | } 276 | 277 | public function testTakeWhile(): void 278 | { 279 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 280 | $taken = $lazyList->takeWhile(static fn (int $i): bool => $i <= 3); 281 | 282 | self::assertEquals(range(1, 3), iterator_to_array($taken)); 283 | } 284 | 285 | public function testTake(): void 286 | { 287 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 288 | $taken = $lazyList->take(5); 289 | 290 | self::assertEquals(range(1, 5), iterator_to_array($taken)); 291 | } 292 | 293 | public function testTakeFilteredInfiniteLazyList(): void 294 | { 295 | $lazyList = LazyList::range(1); 296 | $taken = $lazyList->filter(static fn ($x) => $x < 10)->filter(static fn ($x) => $x >= 5)->take(5); 297 | 298 | self::assertEquals(5, iterator_count($taken)); 299 | } 300 | 301 | public function testChunk(): void 302 | { 303 | $lazyList = LazyList::range(1, 10); 304 | $chunked = $lazyList->chunk(3); 305 | 306 | self::assertEquals( 307 | [ 308 | [1, 2, 3], 309 | [4, 5, 6], 310 | [7, 8, 9], 311 | [10], 312 | ], 313 | $chunked->map(static fn ($chunk) => iterator_to_array($chunk))->toArray(), 314 | ); 315 | } 316 | 317 | public function testHeadOnNotEmptyLazyList(): void 318 | { 319 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 320 | $head = $lazyList->head(); 321 | self::assertTrue($head->isDefined()); 322 | self::assertEquals(1, $head->getUnsafe()); 323 | } 324 | 325 | public function testHeadOnEmptyLazyList(): void 326 | { 327 | /** @var iterable $emptyIterable */ 328 | $emptyIterable = []; 329 | $lazyList = new LazyList($emptyIterable); 330 | $head = $lazyList->head(); 331 | self::assertFalse($head->isDefined()); 332 | } 333 | 334 | public function testLastOnNotEmptyLazyList(): void 335 | { 336 | $lazyList = new LazyList(range(1, 10)); 337 | $last = $lazyList->last(); 338 | self::assertTrue($last->isDefined()); 339 | self::assertEquals(10, $last->getUnsafe()); 340 | } 341 | 342 | public function testLastOnEmptyLazyList(): void 343 | { 344 | /** @var iterable $emptyIterable */ 345 | $emptyIterable = []; 346 | $lazyList = new LazyList($emptyIterable); 347 | $last = $lazyList->last(); 348 | self::assertFalse($last->isDefined()); 349 | } 350 | 351 | public function testFilter(): void 352 | { 353 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 354 | $filtered = $lazyList->filter(static fn (int $item) => $item % 2 === 0); 355 | 356 | self::assertEquals(range(2, 10, 2), iterator_to_array($filtered)); 357 | } 358 | 359 | public function testFindWhenItemExists(): void 360 | { 361 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 362 | $found = $lazyList->find(static fn (int $item) => $item % 2 === 0); 363 | 364 | self::assertTrue($found->isDefined()); 365 | self::assertEquals(2, $found->getUnsafe()); 366 | } 367 | 368 | public function testFindWhenItemDoesNotExist(): void 369 | { 370 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 371 | $found = $lazyList->find(static fn (int $item) => $item === 666); 372 | 373 | self::assertFalse($found->isDefined()); 374 | } 375 | 376 | public function testDropWhile(): void 377 | { 378 | $lazyList = LazyList::range(1, 9)->concat(LazyList::range(0, 5)); 379 | $rest = $lazyList->dropWhile(static fn (int $item) => $item < 5); 380 | 381 | self::assertEquals(array_merge(range(5, 9), range(0, 5)), $rest->toArray()); 382 | } 383 | 384 | public function testDrop(): void 385 | { 386 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 387 | $rest = $lazyList->drop(2); 388 | 389 | self::assertEquals(range(3, 10), $rest->toArray()); 390 | } 391 | 392 | public function testExists(): void 393 | { 394 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 395 | 396 | self::assertTrue($lazyList->exists(static fn (int $item) => $item > 5)); 397 | self::assertFalse($lazyList->exists(static fn (int $item) => $item > 10)); 398 | } 399 | 400 | public function testAll(): void 401 | { 402 | $lazyList = new LazyList(new ArrayIterator(range(2, 10, 2))); 403 | 404 | self::assertTrue($lazyList->all(static fn (int $item) => $item < 11)); 405 | self::assertTrue($lazyList->all(static fn (int $item) => $item % 2 === 0)); 406 | self::assertFalse($lazyList->all(static fn (int $item) => $item < 10)); 407 | } 408 | 409 | public function testZip(): void 410 | { 411 | $lazyList1 = new LazyList(new ArrayIterator(range(1, 10))); 412 | $lazyList2 = new LazyList(new ArrayIterator(range(11, 20))); 413 | 414 | self::assertEquals([ 415 | [1, 11], 416 | [2, 12], 417 | [3, 13], 418 | [4, 14], 419 | [5, 15], 420 | [6, 16], 421 | [7, 17], 422 | [8, 18], 423 | [9, 19], 424 | [10, 20], 425 | ], $lazyList1->zip($lazyList2)->toArray()); 426 | } 427 | 428 | public function testZipMap(): void 429 | { 430 | $ints = LazyList::range(0, 2); 431 | $map = $ints->zipMap(static fn (int $i): string => chr(ord('a') + $i)); 432 | self::assertEquals(Map::fromIterable([[0, 'a'], [1, 'b'], [2, 'c']]), $map); 433 | } 434 | 435 | public function testConcat(): void 436 | { 437 | $lazyList1 = new LazyList(new ArrayIterator(range(1, 10))); 438 | $lazyList2 = new LazyList(new ArrayIterator(range(11, 20))); 439 | $lazyList3 = new LazyList(new ArrayIterator(range(21, 30))); 440 | 441 | self::assertEquals(range(1, 30), $lazyList1->concat($lazyList2, $lazyList3)->toArray()); 442 | } 443 | 444 | public function testAdd(): void 445 | { 446 | $lazyList1 = new LazyList(new ArrayIterator(range(1, 10))); 447 | 448 | self::assertEquals(range(1, 13), $lazyList1->add(11, 12, 13)->toArray()); 449 | } 450 | 451 | public function testInsertOnPosition(): void 452 | { 453 | /** @var LazyList $lazyList1 */ 454 | $lazyList1 = new LazyList([1, 2, 3]); 455 | $lazyList2 = new LazyList(['a', 'b']); 456 | self::assertEquals([1, 'a', 'b', 2, 3], $lazyList1->insertOnPosition(1, $lazyList2)->toArray()); 457 | } 458 | 459 | public function testInsertOnInvalidPosition(): void 460 | { 461 | /** @var LazyList $lazyList1 */ 462 | $lazyList1 = new LazyList([1, 2, 3]); 463 | $lazyList2 = new LazyList(['a', 'b']); 464 | try { 465 | $lazyList1->insertOnPosition(7, $lazyList2)->toArray(); 466 | } catch (InvalidArgumentException $exception) { 467 | self::assertEquals( 468 | 'Tried to insert collection to position 7, but only 3 items were found', 469 | $exception->getMessage(), 470 | ); 471 | } 472 | } 473 | 474 | public function testToArray(): void 475 | { 476 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 477 | 478 | self::assertEquals(range(1, 3), $lazyList->take(3)->toArray()); 479 | } 480 | 481 | public function testToMap(): void 482 | { 483 | $pairs = LazyList::of([1, 'a'], [2, 'b']); 484 | self::assertEquals(Map::fromIterable([[1, 'a'], [2, 'b']]), $pairs->toMap()); 485 | } 486 | 487 | public function testJoin(): void 488 | { 489 | $lazyList = new LazyList([1, 2, 3]); 490 | self::assertEquals('1, 2, 3', $lazyList->join(', ')); 491 | } 492 | 493 | public function testLazyListLazinessChaining(): void 494 | { 495 | $lazyList = new LazyList(new ArrayIterator(range(1, 10))); 496 | $numberOfCalls = 0; 497 | $mapped = $lazyList->map(static function ($item) use (&$numberOfCalls) { 498 | $numberOfCalls++; 499 | return $item * 10; 500 | }); 501 | 502 | self::assertEquals(0, $numberOfCalls); 503 | $taken = $mapped->take(3); 504 | self::assertEquals(0, $numberOfCalls); 505 | self::assertEquals(range(10, 30, 10), iterator_to_array($taken)); 506 | self::assertEquals(3, $numberOfCalls); 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /tests/Bonami/Collection/TrySafeTest.php: -------------------------------------------------------------------------------- 1 | isSuccess()); 19 | 20 | $trySafeFromException = TrySafe::of(new Exception('Exception can act as success value as well')); 21 | self::assertTrue($trySafeFromException->isSuccess()); 22 | } 23 | 24 | public function testCreateFromCallable(): void 25 | { 26 | $returnValueCallable = static fn () => 666; 27 | $throwCallable = static function () { 28 | throw new Exception(); 29 | }; 30 | 31 | self::assertTrue(TrySafe::fromCallable($returnValueCallable)->isSuccess()); 32 | self::assertTrue(TrySafe::fromCallable($throwCallable)->isFailure()); 33 | } 34 | 35 | public function testLift(): void 36 | { 37 | $failure = $this->createFailure(); 38 | $xSuccess = TrySafe::success(1); 39 | $ySuccess = TrySafe::success(4); 40 | 41 | $plus = static fn (int $x, int $y): int => $x + $y; 42 | 43 | $this->equals( 44 | TrySafe::success(5), 45 | TrySafe::lift($plus)($xSuccess, $ySuccess), 46 | ); 47 | $this->equals( 48 | $failure, 49 | TrySafe::lift($plus)($xSuccess, $failure), 50 | ); 51 | } 52 | 53 | public function testMapFailure(): void 54 | { 55 | $success = TrySafe::success(42); 56 | self::assertSame($success, $success->mapFailure(static fn (Throwable $ex) => new Exception())); 57 | 58 | $failure = TrySafe::failure(new Exception('No towel')); 59 | self::assertSame( 60 | 'oops', 61 | $failure->mapFailure(static fn (Throwable $ex) => new Exception('oops'))->getFailureUnsafe()->getMessage(), 62 | ); 63 | } 64 | 65 | public function testMap(): void 66 | { 67 | $mapper = static fn (string $s): string => sprintf('Hello %s', $s); 68 | $mapperThatThrows = static function () { 69 | throw new Exception(); 70 | }; 71 | 72 | $this->equals( 73 | TrySafe::success('Hello world'), 74 | TrySafe::success('world')->map($mapper), 75 | ); 76 | 77 | self::assertTrue(TrySafe::success('Hello world')->map($mapperThatThrows)->isFailure()); 78 | /** @var TrySafe $failure */ 79 | $failure = $this->createFailure(); 80 | self::assertTrue($failure->map($mapper)->isFailure()); 81 | self::assertTrue($failure->map($mapperThatThrows)->isFailure()); 82 | } 83 | 84 | public function testEach(): void 85 | { 86 | $success = TrySafe::success(1); 87 | /** @var TrySafe $failure */ 88 | $failure = TrySafe::failure(new Exception()); 89 | $accumulated = 0; 90 | 91 | $accumulate = static function (int $i) use (&$accumulated): void { 92 | $accumulated += $i; 93 | }; 94 | 95 | $failure->each($accumulate); 96 | self::assertEquals(0, $accumulated); 97 | 98 | $success->each($accumulate); 99 | self::assertEquals(1, $accumulated); 100 | } 101 | 102 | public function testTap(): void 103 | { 104 | $success = TrySafe::success(1); 105 | /** @var TrySafe $failure */ 106 | $failure = TrySafe::failure(new Exception()); 107 | $accumulated = 0; 108 | 109 | $accumulate = static function (int $i) use (&$accumulated): void { 110 | $accumulated += $i; 111 | }; 112 | 113 | self::assertSame($failure, $failure->tap($accumulate)); 114 | self::assertEquals(0, $accumulated); 115 | 116 | self::assertSame($success, $success->tap($accumulate)); 117 | self::assertEquals(1, $accumulated); 118 | } 119 | 120 | public function testTapFailure(): void 121 | { 122 | $success = TrySafe::success(1); 123 | $failure = TrySafe::failure(new Exception('msg')); 124 | 125 | $extractedMessage = ''; 126 | $extractMessage = static function (Throwable $ex) use (&$extractedMessage): void { 127 | $extractedMessage = $ex->getMessage(); 128 | }; 129 | 130 | self::assertSame($success, $success->tapFailure($extractMessage)); 131 | self::assertEquals('', $extractedMessage); 132 | 133 | self::assertSame($failure, $failure->tapFailure($extractMessage)); 134 | self::assertEquals('msg', $extractedMessage); 135 | } 136 | 137 | public function testFlatMap(): void 138 | { 139 | 140 | $mapperToFailure = fn(string $s): TrySafe => $this->createFailure(); 141 | 142 | $mapper = static fn (bool $shouldSucceed) => static fn (string $s) => $shouldSucceed 143 | ? TrySafe::success(sprintf('Hello %s', $s)) 144 | : throw new Exception(); 145 | 146 | $this->equals( 147 | TrySafe::success('Hello world'), 148 | TrySafe::success('world')->flatMap($mapper(true)), 149 | ); 150 | 151 | /** @var TrySafe $trySafe */ 152 | $trySafe = $this->createFailure(); 153 | self::assertTrue($trySafe->flatMap($mapper(true))->isFailure()); 154 | self::assertTrue(TrySafe::success('world')->flatMap($mapperToFailure)->isFailure()); 155 | self::assertTrue($trySafe->flatMap($mapperToFailure)->isFailure()); 156 | self::assertTrue(TrySafe::success('world')->flatMap($mapper(false))->isFailure()); 157 | } 158 | 159 | public function testRecover(): void 160 | { 161 | $failure = new Exception(); 162 | 163 | $recoveredFailure = TrySafe::failure($failure) 164 | ->recover(static fn (Throwable $failure): int => 666); 165 | self::assertTrue($recoveredFailure->isSuccess()); 166 | 167 | $exceptionThatRecoveryThrows = new Exception(); 168 | $recoveryEndedWithFailure = TrySafe::failure($failure) 169 | ->recover(static function (Throwable $failure) use ($exceptionThatRecoveryThrows) { 170 | throw $exceptionThatRecoveryThrows; 171 | }); 172 | 173 | self::assertTrue($recoveryEndedWithFailure->isFailure()); 174 | self::assertSame($exceptionThatRecoveryThrows, $recoveryEndedWithFailure->getFailureUnsafe()); 175 | } 176 | 177 | public function testRecoverIf(): void 178 | { 179 | $failure = new Exception(); 180 | 181 | self::assertTrue(TrySafe::failure($failure) 182 | ->recoverIf( 183 | tautology(), 184 | static fn (Throwable $failure): int => 666, 185 | ) 186 | ->isSuccess()); 187 | 188 | self::assertTrue(TrySafe::failure($failure) 189 | ->recoverIf( 190 | falsy(), 191 | static fn (Throwable $failure): int => 666, 192 | ) 193 | ->isFailure()); 194 | 195 | $exceptionThatRecoveryThrows = new Exception(); 196 | $recoveryEndedWithFailure = TrySafe::failure($failure) 197 | ->recoverIf(tautology(), static function (Throwable $failure) use ($exceptionThatRecoveryThrows) { 198 | throw $exceptionThatRecoveryThrows; 199 | }); 200 | 201 | self::assertTrue($recoveryEndedWithFailure->isFailure()); 202 | self::assertSame($exceptionThatRecoveryThrows, $recoveryEndedWithFailure->getFailureUnsafe()); 203 | } 204 | 205 | public function testRecoverWith(): void 206 | { 207 | $success = TrySafe::success(42); 208 | /** @var TrySafe $failure */ 209 | $failure = TrySafe::failure(new Exception()); 210 | 211 | $recover = static fn (Throwable $ex): TrySafe => TrySafe::success(666); 212 | $exceptionThatRecoveryThrows = new Exception(); 213 | $throw = static function (Throwable $ex) use ($exceptionThatRecoveryThrows) { 214 | throw $exceptionThatRecoveryThrows; 215 | }; 216 | $exceptionThatRecoveryWraps = new Exception(); 217 | 218 | $wrap = static fn (Throwable $failure): TrySafe => TrySafe::failure($exceptionThatRecoveryWraps); 219 | 220 | self::assertSame(42, $success->recoverWith($recover)->getUnsafe()); 221 | self::assertTrue($success->recoverWith($recover)->isSuccess()); 222 | 223 | self::assertSame(42, $success->recoverWith($throw)->getUnsafe()); 224 | self::assertTrue($success->recoverWith($throw)->isSuccess()); 225 | 226 | self::assertSame(42, $success->recoverWith($wrap)->getUnsafe()); 227 | self::assertTrue($success->recoverWith($wrap)->isSuccess()); 228 | 229 | self::assertTrue($failure->recoverWith($recover)->isSuccess()); 230 | self::assertSame(666, $failure->recoverWith($recover)->getUnsafe()); 231 | 232 | self::assertTrue($failure->recoverWith($throw)->isFailure()); 233 | self::assertSame($exceptionThatRecoveryThrows, $failure->recoverWith($throw)->getFailureUnsafe()); 234 | 235 | self::assertTrue($failure->recoverWith($wrap)->isFailure()); 236 | self::assertSame($exceptionThatRecoveryWraps, $failure->recoverWith($wrap)->getFailureUnsafe()); 237 | } 238 | 239 | public function testRecoverWithIf(): void 240 | { 241 | $originalException = new Exception(); 242 | $success = TrySafe::success(42); 243 | /** @var TrySafe $failure */ 244 | $failure = TrySafe::failure($originalException); 245 | 246 | $recover = static fn (Throwable $ex): TrySafe => TrySafe::success(666); 247 | $matchRuntimeException = static fn (Throwable $throwable): bool => $throwable instanceof RuntimeException; 248 | $matchAll = static fn (Throwable $throwable): bool => true; 249 | $exceptionThatRecoveryThrows = new Exception(); 250 | $throw = static function (Throwable $ex) use ($exceptionThatRecoveryThrows) { 251 | throw $exceptionThatRecoveryThrows; 252 | }; 253 | $exceptionThatRecoveryWraps = new Exception(); 254 | 255 | $wrap = static fn (Throwable $failure) => TrySafe::failure($exceptionThatRecoveryWraps); 256 | 257 | self::assertSame(42, $success->recoverWithIf($matchRuntimeException, $recover)->getUnsafe()); 258 | self::assertTrue($success->recoverWithIf($matchRuntimeException, $recover)->isSuccess()); 259 | 260 | self::assertSame(42, $success->recoverWithIf($matchRuntimeException, $throw)->getUnsafe()); 261 | self::assertTrue($success->recoverWithIf($matchRuntimeException, $throw)->isSuccess()); 262 | 263 | self::assertSame(42, $success->recoverWithIf($matchRuntimeException, $wrap)->getUnsafe()); 264 | self::assertTrue($success->recoverWithIf($matchRuntimeException, $wrap)->isSuccess()); 265 | 266 | self::assertTrue($failure->recoverWithIf($matchRuntimeException, $recover)->isFailure()); 267 | self::assertTrue($failure->recoverWithIf($matchAll, $recover)->isSuccess()); 268 | self::assertSame(666, $failure->recoverWithIf($matchAll, $recover)->getUnsafe()); 269 | 270 | self::assertTrue($failure->recoverWithIf($matchAll, $throw)->isFailure()); 271 | self::assertSame($exceptionThatRecoveryThrows, $failure->recoverWithIf($matchAll, $throw)->getFailureUnsafe()); 272 | self::assertSame( 273 | $originalException, 274 | $failure->recoverWithIf($matchRuntimeException, $throw)->getFailureUnsafe(), 275 | ); 276 | 277 | self::assertTrue($failure->recoverWithIf($matchRuntimeException, $wrap)->isFailure()); 278 | self::assertTrue($failure->recoverWithIf($matchAll, $wrap)->isFailure()); 279 | self::assertSame($exceptionThatRecoveryWraps, $failure->recoverWithIf($matchAll, $wrap)->getFailureUnsafe()); 280 | } 281 | 282 | public function testToOption(): void 283 | { 284 | $value = 666; 285 | self::assertEquals($value, TrySafe::success($value)->toOption()->getUnsafe()); 286 | self::assertFalse($this->createFailure()->toOption()->isDefined()); 287 | } 288 | 289 | public function testResolveSuccess(): void 290 | { 291 | $handleSuccessSpy = createInvokableSpy(); 292 | $handleFailureSpy = createInvokableSpy(); 293 | 294 | TrySafe::success(666) 295 | ->resolve($handleFailureSpy, $handleSuccessSpy); 296 | 297 | self::assertCount(1, $handleSuccessSpy->getCalls()); 298 | self::assertCount(0, $handleFailureSpy->getCalls()); 299 | self::assertEquals([[666]], $handleSuccessSpy->getCalls()); 300 | } 301 | 302 | public function testResolveFailure(): void 303 | { 304 | $handleSuccessSpy = createInvokableSpy(); 305 | $handleFailureSpy = createInvokableSpy(); 306 | 307 | $exception = new Exception(); 308 | TrySafe::failure($exception) 309 | ->resolve($handleFailureSpy, $handleSuccessSpy); 310 | 311 | self::assertCount(0, $handleSuccessSpy->getCalls()); 312 | self::assertCount(1, $handleFailureSpy->getCalls()); 313 | self::assertSame([[$exception]], $handleFailureSpy->getCalls()); 314 | } 315 | 316 | public function testIterator(): void 317 | { 318 | $val = 'Hello world'; 319 | $success = TrySafe::success($val); 320 | $failure = $this->createFailure(); 321 | 322 | self::assertEquals([$val], iterator_to_array($success->getIterator(), false)); 323 | self::assertEquals([], iterator_to_array($failure->getIterator(), false)); 324 | } 325 | 326 | public function testReduce(): void 327 | { 328 | $reducer = static fn (int $reduction, int $val): int => $reduction + $val; 329 | $initialReduction = 4; 330 | 331 | self::assertEquals( 332 | 670, 333 | TrySafe::success(666)->reduce($reducer, $initialReduction), 334 | ); 335 | self::assertEquals( 336 | $initialReduction, 337 | $this->createFailure()->reduce($reducer, $initialReduction), 338 | ); 339 | } 340 | 341 | public function testLaws(): void 342 | { 343 | $assertEquals = function ($a, $b): void { 344 | $this->equals($a, $b); 345 | }; 346 | $tryEquals = static fn (TrySafe $a, TrySafe $b): bool => $a->equals($b); 347 | $ap = TrySafe::ap(...); 348 | $pure = TrySafe::pure(...); 349 | 350 | $successOne = TrySafe::success(1); 351 | $successTwo = TrySafe::success(2); 352 | $successThree = TrySafe::success(3); 353 | $failure = $this->createFailure(); 354 | 355 | $plus2 = CurriedFunction::of(static fn (int $x): int => $x + 2); 356 | $multiple2 = CurriedFunction::of(static fn (int $x): int => $x * 2); 357 | $throws = CurriedFunction::of(function (int $_) { 358 | throw $this->createHashableException(); 359 | }); 360 | 361 | testEqualsReflexivity($assertEquals, $tryEquals, $successOne); 362 | testEqualsReflexivity($assertEquals, $tryEquals, $failure); 363 | 364 | testEqualsSymmetry($assertEquals, $tryEquals, $successOne, $successOne); 365 | testEqualsSymmetry($assertEquals, $tryEquals, $successOne, $successTwo); 366 | testEqualsSymmetry($assertEquals, $tryEquals, $successOne, $failure); 367 | testEqualsSymmetry($assertEquals, $tryEquals, $failure, $failure); 368 | 369 | testEqualsTransitivity($assertEquals, $tryEquals, $successOne, $successOne, $successOne); 370 | testEqualsTransitivity($assertEquals, $tryEquals, $successOne, $successTwo, $successThree); 371 | testEqualsTransitivity($assertEquals, $tryEquals, $successOne, $failure, $successThree); 372 | 373 | testFunctorIdentity($assertEquals, $successOne); 374 | testFunctorIdentity($assertEquals, $failure); 375 | 376 | testFunctorComposition($assertEquals, $successOne, $plus2, $multiple2); 377 | testFunctorComposition($assertEquals, $failure, $plus2, $multiple2); 378 | testFunctorComposition($assertEquals, $successOne, $plus2, $throws); 379 | testFunctorComposition($assertEquals, $successOne, $throws, $multiple2); 380 | testFunctorComposition($assertEquals, $failure, $plus2, $throws); 381 | testFunctorComposition($assertEquals, $failure, $throws, $multiple2); 382 | testFunctorComposition($assertEquals, $successOne, $throws, $throws); 383 | testFunctorComposition($assertEquals, $failure, $throws, $throws); 384 | 385 | testApplicativeIdentity($assertEquals, $ap, $pure, $successOne); 386 | testApplicativeIdentity($assertEquals, $ap, $pure, $failure); 387 | 388 | testApplicativeHomomorphism($assertEquals, $ap, $pure, 666, $multiple2); 389 | testApplicativeHomomorphism($assertEquals, $ap, $pure, 666, $multiple2); 390 | 391 | testApplicativeComposition($assertEquals, $ap, $pure, $successOne, $pure($plus2), $pure($multiple2)); 392 | testApplicativeComposition($assertEquals, $ap, $pure, $failure, $pure($plus2), $pure($multiple2)); 393 | testApplicativeComposition($assertEquals, $ap, $pure, $successOne, $failure, $pure($multiple2)); 394 | testApplicativeComposition($assertEquals, $ap, $pure, $failure, $pure($plus2), $failure); 395 | 396 | testApplicativeInterchange($assertEquals, $ap, $pure, 666, $pure($plus2)); 397 | testApplicativeInterchange($assertEquals, $ap, $pure, 666, $pure($throws)); 398 | testApplicativeInterchange($assertEquals, $ap, $pure, 666, $failure); 399 | } 400 | 401 | /** 402 | * @template T 403 | * 404 | * @phpstan-param T $a 405 | * @phpstan-param T $b 406 | * 407 | * @phpstan-return void 408 | */ 409 | private function equals($a, $b): void 410 | { 411 | if ($a instanceof TrySafe && $b instanceof TrySafe) { 412 | self::assertTrue($a->equals($b)); 413 | } else { 414 | self::assertEquals($a, $b); 415 | } 416 | } 417 | 418 | /** @phpstan-return TrySafe */ 419 | private function createFailure(): TrySafe 420 | { 421 | return TrySafe::failure($this->createHashableException()); 422 | } 423 | 424 | private function createHashableException(): Throwable 425 | { 426 | return new class extends Exception implements IHashable { 427 | public function hashCode(): string 428 | { 429 | return self::class; 430 | } 431 | }; 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/Bonami/Collection/LazyList.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class LazyList implements IteratorAggregate 21 | { 22 | /** @use Monad1 */ 23 | use Monad1; 24 | /** @use Iterable1 */ 25 | use Iterable1; 26 | 27 | /** @var iterable */ 28 | private $items; 29 | 30 | /** @param iterable $iterable */ 31 | final public function __construct(iterable $iterable) 32 | { 33 | $this->items = $iterable; 34 | } 35 | 36 | /** 37 | * @param int $low 38 | * @param int $high 39 | * @param int $step 40 | * 41 | * @return self 42 | */ 43 | public static function range(int $low, int $high = PHP_INT_MAX, int $step = 1): self 44 | { 45 | $range = static function (int $low, int $high, int $step = 1): Generator { 46 | while ($low <= $high) { 47 | yield $low; 48 | $low += $step; 49 | } 50 | }; 51 | 52 | return new self($range($low, $high, $step)); 53 | } 54 | 55 | /** 56 | * @template A 57 | * 58 | * @param A $item 59 | * @param int|null $size - When no size is passed, infinite items are filled (lazily) 60 | * 61 | * @return static 62 | */ 63 | public static function fill($item, ?int $size = null) 64 | { 65 | $fill = static function ($item, ?int $size = null): Generator { 66 | $generated = 0; 67 | while ($size === null || $size > $generated) { 68 | yield $item; 69 | if ($size !== null) { 70 | $generated++; 71 | } 72 | } 73 | }; 74 | 75 | return new static($fill($item, $size)); 76 | } 77 | 78 | /** @return static */ 79 | public static function fromEmpty() 80 | { 81 | /** @var array $empty */ 82 | $empty = []; 83 | 84 | return new static($empty); 85 | } 86 | 87 | /** 88 | * @template V 89 | * 90 | * @param iterable $iterable 91 | * 92 | * @return static 93 | */ 94 | public static function fromIterable(iterable $iterable) 95 | { 96 | return new static($iterable); 97 | } 98 | 99 | /** 100 | * @template V 101 | * 102 | * @param V ...$items 103 | * 104 | * @return static 105 | */ 106 | public static function of(...$items) 107 | { 108 | return new static(array_values($items)); 109 | } 110 | 111 | /** 112 | * @template V 113 | * 114 | * @param V $item 115 | * 116 | * @return static 117 | */ 118 | public static function pure($item) 119 | { 120 | return new static([$item]); 121 | } 122 | 123 | /** 124 | * @template B 125 | * 126 | * @param callable(T, int): B $mapper 127 | * 128 | * @return self 129 | */ 130 | public function map(callable $mapper): self 131 | { 132 | $map = function (callable $callback): Generator { 133 | foreach ($this->items as $key => $item) { 134 | yield $callback($item, $key); 135 | } 136 | }; 137 | return new self($map($mapper)); 138 | } 139 | 140 | /** 141 | * @template B 142 | * 143 | * @param callable(T, int): iterable $mapper 144 | * 145 | * @return self 146 | */ 147 | public function flatMap(callable $mapper): self 148 | { 149 | return $this->map($mapper)->flatten(); 150 | } 151 | 152 | /** @return self */ 153 | public function flatten(): self 154 | { 155 | $flatten = function (): Generator { 156 | foreach ($this->items as $item) { 157 | if (is_iterable($item)) { 158 | yield from $item; 159 | } else { 160 | throw new RuntimeException('Some item cannot be flattened because it is not iterable'); 161 | } 162 | } 163 | }; 164 | return new self($flatten()); 165 | } 166 | 167 | /** @param callable(T, int): void $sideEffect */ 168 | public function each(callable $sideEffect): void 169 | { 170 | foreach ($this->items as $key => $item) { 171 | $sideEffect($item, $key); 172 | } 173 | } 174 | 175 | /** 176 | * Defers execution of $sideEffect on each item of LazyList without materializing. Returns a new LazyList with same 177 | * contents (same unchanged items) 178 | * 179 | * Allows inserting side-effects in a chain of method calls. 180 | * 181 | * Also allows executing multiple side-effects on same LazyList 182 | * 183 | * Complexity: o(n) 184 | * 185 | * @param callable(T, int): void $sideEffect 186 | * 187 | * @return static 188 | */ 189 | public function tap(callable $sideEffect) 190 | { 191 | $tap = function () use ($sideEffect): Generator { 192 | foreach ($this->items as $key => $item) { 193 | $sideEffect($item, $key); 194 | yield $key => $item; 195 | } 196 | }; 197 | 198 | return new self($tap()); 199 | } 200 | 201 | /** 202 | * Computes reduction of the elements of the collection. 203 | * 204 | * @template R 205 | * 206 | * @param callable(R, T, int): R $reducer a binary operation for reduction 207 | * @param R $initialReduction 208 | * 209 | * @return R 210 | */ 211 | public function reduce(callable $reducer, $initialReduction) 212 | { 213 | $reduction = $initialReduction; 214 | foreach ($this->items as $key => $item) { 215 | $reduction = $reducer($reduction, $item, $key); 216 | } 217 | return $reduction; 218 | } 219 | 220 | /** 221 | * Reduce (folds) List to single value using Monoid 222 | * 223 | * Complexity: o(n) 224 | * 225 | * @see sum - for trivial summing 226 | * 227 | * @param Monoid $monoid 228 | * 229 | * @return T 230 | */ 231 | public function mfold(Monoid $monoid) 232 | { 233 | $reduction = $monoid->getEmpty(); 234 | foreach ($this->items as $item) { 235 | $reduction = $monoid->concat($reduction, $item); 236 | } 237 | return $reduction; 238 | } 239 | 240 | /** 241 | * Converts items to numbers and then sums them up. 242 | * 243 | * Complexity: o(n) 244 | * 245 | * @see mfold - for folding diferent types of items (E.g. classes representing BigNumbers and so on) 246 | * 247 | * @param callable(T): (int|float) $itemToNumber 248 | * 249 | * @return int|float 250 | */ 251 | public function sum(callable $itemToNumber) 252 | { 253 | $reduction = 0; 254 | foreach ($this->items as $item) { 255 | $reduction += $itemToNumber($item); 256 | } 257 | return $reduction; 258 | } 259 | 260 | /** 261 | * Computes a prefix scan (reduction) of the elements of the collection. 262 | * 263 | * @template R 264 | * 265 | * @param callable(R, T, int): R $scanner a binary operation for scan (reduction) 266 | * @param R $initialReduction 267 | * 268 | * @return self collection with intermediate scan (reduction) results 269 | */ 270 | public function scan(callable $scanner, $initialReduction): self 271 | { 272 | $scan = function (callable $scanner, $initialReduction): Generator { 273 | $prefixReduction = $initialReduction; 274 | foreach ($this->items as $key => $item) { 275 | $prefixReduction = $scanner($prefixReduction, $item, $key); 276 | yield $prefixReduction; 277 | } 278 | }; 279 | 280 | return new self($scan($scanner, $initialReduction)); 281 | } 282 | 283 | /** 284 | * Materialize lazy list and executes items until it hits predicate 285 | * 286 | * Can be used in combination with tap to execute side effect 287 | * and early break after resolving last input passed to tap 288 | * against predicate. 289 | * 290 | * @phpstan-param callable(T, int): bool $predicate 291 | * 292 | * @phpstan-return void 293 | */ 294 | public function doWhile(callable $predicate): void 295 | { 296 | foreach ($this->items as $key => $item) { 297 | if (!$predicate($item, $key)) { 298 | break; 299 | } 300 | } 301 | } 302 | 303 | /** 304 | * Materialize lazy list 305 | * 306 | * This is useful mainly when lazy list 307 | * contains some side effects in its chain which you need to execute. 308 | * 309 | * @see doWhile for materializing until some predicate is matched (for early break) 310 | * @see each for executing the side effect with direct materialization 311 | * @see tap for delayed side effect chaining without direct materialization 312 | */ 313 | public function run(): void 314 | { 315 | // phpcs:ignore 316 | foreach ($this->items as $i) { 317 | // noop, simply to start materialization 318 | } 319 | } 320 | 321 | /** 322 | * @param callable(T, int): bool $predicate 323 | * 324 | * @return static 325 | */ 326 | public function takeWhile(callable $predicate) 327 | { 328 | $takeWhile = function (callable $whileCallback): Generator { 329 | foreach ($this->items as $key => $item) { 330 | if (!$whileCallback($item, $key)) { 331 | break; 332 | } 333 | yield $item; 334 | } 335 | }; 336 | return new static($takeWhile($predicate)); 337 | } 338 | 339 | /** 340 | * @param int $size 341 | * 342 | * @return static 343 | */ 344 | public function take(int $size) 345 | { 346 | $take = function (int $size): Generator { 347 | $taken = 1; 348 | foreach ($this->items as $item) { 349 | yield $item; 350 | $taken++; 351 | if ($taken > $size) { 352 | break; 353 | } 354 | } 355 | }; 356 | return new static($take($size)); 357 | } 358 | 359 | /** @return self> */ 360 | public function chunk(int $size): self 361 | { 362 | assert($size > 0, 'Size must be positive'); 363 | $chunk = function (int $size): Generator { 364 | $materializedChunk = []; 365 | foreach ($this->items as $item) { 366 | $materializedChunk[] = $item; 367 | if (count($materializedChunk) === $size) { 368 | yield static::fromIterable($materializedChunk); 369 | $materializedChunk = []; 370 | } 371 | } 372 | 373 | if (count($materializedChunk) !== 0) { 374 | yield static::fromIterable($materializedChunk); 375 | } 376 | }; 377 | 378 | return new self($chunk($size)); 379 | } 380 | 381 | /** @return Option */ 382 | public function head(): Option 383 | { 384 | return $this->find(static fn ($_): bool => true); 385 | } 386 | 387 | /** @return Option */ 388 | public function last(): Option 389 | { 390 | $isEmpty = true; 391 | $last = null; 392 | foreach ($this->items as $item) { 393 | $isEmpty = false; 394 | $last = $item; 395 | } 396 | 397 | return $isEmpty 398 | ? Option::none() 399 | : Option::some($last); 400 | } 401 | 402 | /** 403 | * @param callable(T, int): bool $predicate 404 | * 405 | * @return static 406 | */ 407 | public function filter(callable $predicate) 408 | { 409 | $filter = function (callable $predicate): Generator { 410 | foreach ($this->items as $key => $item) { 411 | if ($predicate($item, $key)) { 412 | yield $item; 413 | } 414 | } 415 | }; 416 | return new static($filter($predicate)); 417 | } 418 | 419 | /** 420 | * @param callable(T, int): bool $predicate 421 | * 422 | * @return Option 423 | */ 424 | public function find(callable $predicate): Option 425 | { 426 | foreach ($this->items as $key => $item) { 427 | if ($predicate($item, $key)) { 428 | return Option::some($item); 429 | } 430 | } 431 | 432 | return Option::none(); 433 | } 434 | 435 | /** 436 | * @param callable(T, int): bool $predicate 437 | * 438 | * @return static 439 | */ 440 | public function dropWhile(callable $predicate) 441 | { 442 | $drop = function (callable $dropCallback): Generator { 443 | $dropping = true; 444 | foreach ($this->items as $key => $item) { 445 | if ($dropping && $dropCallback($item, $key)) { 446 | continue; 447 | } 448 | 449 | $dropping = false; 450 | 451 | yield $item; 452 | } 453 | }; 454 | 455 | return new static($drop($predicate)); 456 | } 457 | 458 | /** 459 | * @param int $count 460 | * 461 | * @return static 462 | */ 463 | public function drop(int $count) 464 | { 465 | $i = 0; 466 | return $this->dropWhile(static function ($_) use ($count, &$i): bool { 467 | return $i++ < $count; 468 | }); 469 | } 470 | 471 | /** 472 | * @param callable(T, int): bool $predicate 473 | * 474 | * @return bool 475 | */ 476 | public function exists(callable $predicate): bool 477 | { 478 | foreach ($this->items as $key => $item) { 479 | if ($predicate($item, $key)) { 480 | return true; 481 | } 482 | } 483 | 484 | return false; 485 | } 486 | 487 | /** 488 | * @param callable(T, int): bool $predicate 489 | * 490 | * @return bool 491 | */ 492 | public function all(callable $predicate): bool 493 | { 494 | foreach ($this->items as $key => $item) { 495 | if (!$predicate($item, $key)) { 496 | return false; 497 | } 498 | } 499 | 500 | return true; 501 | } 502 | 503 | /** 504 | * @template B 505 | * 506 | * @param iterable $iterable 507 | * 508 | * @return self 509 | */ 510 | public function zip(iterable $iterable): self 511 | { 512 | $rewind = static function (Iterator $iterator): void { 513 | $iterator->rewind(); 514 | }; 515 | $isValid = static fn (Iterator $iterator): bool => $iterator->valid(); 516 | $moveNext = static function (Iterator $iterator): void { 517 | $iterator->next(); 518 | }; 519 | $zip = function (iterable $iterable) use ($rewind, $isValid, $moveNext): Generator { 520 | $traversables = self::of($this->getIterator(), $this->createIterator($iterable)); 521 | 522 | $traversables->each($rewind); 523 | while ($traversables->all($isValid)) { 524 | yield $traversables->map(static fn (Iterator $iterator) => $iterator->current())->toArray(); 525 | $traversables->each($moveNext); 526 | } 527 | }; 528 | return new self($zip($iterable)); 529 | } 530 | 531 | /** 532 | * Maps with $mapper and zips with original values to keep track of which original value was mapped with $mapper 533 | * 534 | * This operation immediately materializes LazyList 535 | * 536 | * Complexity: o(n) 537 | * 538 | * @template B 539 | * 540 | * @param callable(T, int): B $mapper 541 | * 542 | * @return Map 543 | */ 544 | public function zipMap(callable $mapper): Map 545 | { 546 | return $this 547 | ->map(static fn ($value, $key): array => [$value, $mapper($value, $key)]) 548 | ->toMap(); 549 | } 550 | 551 | /** 552 | * @template T2 553 | * 554 | * @param iterable ...$iterables 555 | * 556 | * @return static 557 | */ 558 | public function concat(iterable ...$iterables) 559 | { 560 | $append = function (array $iterables): Generator { 561 | yield from $this; 562 | foreach ($iterables as $iterator) { 563 | yield from $iterator; 564 | } 565 | }; 566 | return new static($append($iterables)); 567 | } 568 | 569 | /** 570 | * @param T ...$items 571 | * 572 | * @return static 573 | */ 574 | public function add(...$items) 575 | { 576 | return $this->concat(new self(array_values($items))); 577 | } 578 | 579 | /** 580 | * @param int $position 581 | * @param iterable $iterable 582 | * 583 | * @return static 584 | */ 585 | public function insertOnPosition(int $position, iterable $iterable) 586 | { 587 | $insertOnPosition = function (int $position, iterable $iterable): Generator { 588 | $index = 0; 589 | foreach ($this->items as $item) { 590 | if ($index === $position) { 591 | yield from $iterable; 592 | } 593 | $index++; 594 | yield $item; 595 | } 596 | if ($position >= $index) { 597 | throw new InvalidArgumentException(sprintf( 598 | 'Tried to insert collection to position %d, but only %d items were found', 599 | $position, 600 | $index, 601 | )); 602 | } 603 | }; 604 | return new static($insertOnPosition($position, $iterable)); 605 | } 606 | 607 | /** @return array */ 608 | public function toArray(): array 609 | { 610 | return iterator_to_array($this->getIterator(), false); 611 | } 612 | 613 | public function join(string $glue): string 614 | { 615 | return implode($glue, $this->toArray()); 616 | } 617 | 618 | /** @return Iterator */ 619 | public function getIterator(): Iterator 620 | { 621 | return $this->createIterator($this->items); 622 | } 623 | 624 | /** @return ArrayList */ 625 | public function toList(): ArrayList 626 | { 627 | return ArrayList::fromIterable($this); 628 | } 629 | 630 | /** 631 | * Creates a map from List of pairs. 632 | * 633 | * When called, you have to be sure, that list contains two element arrays, otherwise it will fails in runtime 634 | * with exception. 635 | * 636 | * Complexity: o(n) 637 | * 638 | * @return Map 639 | */ 640 | public function toMap(): Map 641 | { 642 | /** @var array */ 643 | $pairs = $this->toArray(); 644 | return Map::fromIterable($pairs); 645 | } 646 | 647 | /** 648 | * @param iterable $iterable 649 | * 650 | * @return Iterator 651 | */ 652 | private function createIterator(iterable $iterable): Iterator 653 | { 654 | if ($iterable instanceof Iterator) { 655 | return $iterable; 656 | } 657 | 658 | if ($iterable instanceof IteratorAggregate) { 659 | return $iterable->getIterator(); 660 | } 661 | 662 | if (is_array($iterable)) { 663 | return new ArrayIterator($iterable); 664 | } 665 | 666 | // Fallback to generator, be aware, that it is not rewindable! 667 | return (static function (iterable $iterable): Generator { 668 | yield from $iterable; 669 | })($iterable); 670 | } 671 | } 672 | --------------------------------------------------------------------------------