├── docs ├── assets │ ├── favicon.png │ ├── fonts │ │ └── MonaspaceNeon-Regular.woff │ ├── logo.svg │ └── stylesheets │ │ └── extra.css ├── use-cases │ ├── index.md │ ├── parsing.md │ └── lazy-file.md ├── README.md ├── structures │ ├── index.md │ ├── regexp.md │ ├── state.md │ ├── identity.md │ └── fold.md ├── testing.md ├── PHILOSOPHY.md └── MONOIDS.md ├── src ├── Exception │ ├── Exception.php │ ├── LogicException.php │ └── InvalidRegex.php ├── Predicate.php ├── Attempt │ ├── Guard.php │ ├── Result.php │ ├── Implementation.php │ ├── Error.php │ └── Defer.php ├── Monoid.php ├── Monoid │ ├── Concat.php │ ├── ArrayMerge.php │ ├── MergeSet.php │ ├── Append.php │ └── MergeMap.php ├── Str │ └── Encoding.php ├── SideEffect.php ├── Pair.php ├── Identity │ ├── Implementation.php │ ├── InMemory.php │ ├── Lazy.php │ └── Defer.php ├── State │ └── Result.php ├── Validation │ ├── Guard.php │ ├── Implementation.php │ └── Success.php ├── RegisterCleanup.php ├── Sequence │ ├── Sink │ │ └── Continuation.php │ ├── Iterator │ │ ├── Primitive.php │ │ ├── Defer.php │ │ └── Lazy.php │ ├── Iterator.php │ ├── Sink.php │ └── Aggregate.php ├── Predicate │ ├── Instance.php │ ├── OrPredicate.php │ └── AndPredicate.php ├── Fold │ ├── Implementation.php │ ├── Result.php │ ├── Failure.php │ └── With.php ├── Maybe │ ├── Nothing.php │ ├── Comprehension.php │ ├── Implementation.php │ ├── Just.php │ └── Defer.php ├── State.php ├── RegExp.php ├── Either │ ├── Implementation.php │ ├── Left.php │ ├── Right.php │ └── Defer.php ├── Identity.php ├── Fold.php ├── Map │ ├── Implementation.php │ └── Uninitialized.php ├── Accumulate.php ├── Validation.php ├── Either.php └── Maybe.php ├── proofs ├── phpunit.php ├── monoid │ ├── concat.php │ ├── append.php │ ├── mergeSet.php │ ├── mergeMap.php │ └── arrayMerge.php ├── map.php ├── either.php ├── predicate.php └── maybe.php ├── .php-cs-fixer.dist.php ├── .github └── workflows │ ├── release.yml │ ├── documentation.yml │ └── ci.yml ├── Makefile ├── psalm.xml ├── blackbox.php ├── properties ├── Sequence.php ├── Monoid.php ├── Monoid │ ├── Associativity.php │ └── Identity.php └── Sequence │ └── Windows.php ├── fixtures ├── Sequence.php └── Set.php ├── LICENSE ├── composer.json ├── mkdocs.yml └── README.md /docs/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Innmind/Immutable/HEAD/docs/assets/favicon.png -------------------------------------------------------------------------------- /docs/assets/fonts/MonaspaceNeon-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Innmind/Immutable/HEAD/docs/assets/fonts/MonaspaceNeon-Regular.woff -------------------------------------------------------------------------------- /docs/use-cases/index.md: -------------------------------------------------------------------------------- 1 | # Use cases 2 | 3 | In this chapter you'll find a set of use cases using data structures already shown in previous chapters. 4 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | e; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | - toc 5 | --- 6 | 7 | # Getting Started 8 | 9 | This project brings a set of immutable data structure to bring a uniformity on how to handle data. 10 | 11 | Before diving in the documentation you may want to read about the [philosophy](PHILOSOPHY.md) behind the structures design. 12 | 13 | ## Installation 14 | 15 | ```sh 16 | composer require innmind/immutable 17 | ``` 18 | -------------------------------------------------------------------------------- /src/Monoid.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class Concat implements Monoid 16 | { 17 | #[\Override] 18 | public function identity(): Str 19 | { 20 | return Str::of(''); 21 | } 22 | 23 | #[\Override] 24 | public function combine(mixed $a, mixed $b): Str 25 | { 26 | return $a->append($b->toString()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Str/Encoding.php: -------------------------------------------------------------------------------- 1 | 'UTF-8', 24 | self::utf16 => 'UTF-16', 25 | self::utf32 => 'UTF-32', 26 | self::ascii => 'ASCII', 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/structures/index.md: -------------------------------------------------------------------------------- 1 | # Structures 2 | 3 | This library provides the following structures: 4 | 5 | - [`Sequence`](sequence.md) 6 | - [`Set`](set.md) 7 | - [`Map`](map.md) 8 | - [`Str`](str.md) 9 | - [`RegExp`](regexp.md) 10 | - [`Maybe`](maybe.md) 11 | - [`Either`](either.md) 12 | - [`Attempt`](attempt.md) 13 | - [`Validation`](validation.md) 14 | - [`Identity`](identity.md) 15 | - [`State`](state.md) 16 | - [`Fold`](fold.md) 17 | 18 | See the documentation for each structure to understand how to use them. 19 | 20 | All structures are typed with [`vimeo/psalm`](https://psalm.dev), you must use it in order to verify that you use this library correctly. 21 | -------------------------------------------------------------------------------- /src/Monoid/ArrayMerge.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | final class ArrayMerge implements Monoid 15 | { 16 | #[\Override] 17 | public function identity(): mixed 18 | { 19 | /** @var array */ 20 | return []; 21 | } 22 | 23 | /** 24 | * @param array $a 25 | * @param array $b 26 | * 27 | * @return array 28 | */ 29 | #[\Override] 30 | public function combine(mixed $a, mixed $b): mixed 31 | { 32 | return \array_replace($a, $b); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SideEffect.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /blackbox.php: -------------------------------------------------------------------------------- 1 | when( 14 | \getenv('ENABLE_COVERAGE') !== false, 15 | static fn(Application $app) => $app 16 | ->codeCoverage( 17 | CodeCoverage::of( 18 | __DIR__.'/src/', 19 | __DIR__.'/proofs/', 20 | __DIR__.'/fixtures/', 21 | ) 22 | ->dumpTo('coverage.clover') 23 | ->enableWhen(true), 24 | ) 25 | ->scenariiPerProof(50), 26 | ) 27 | ->tryToProve(Load::everythingIn(__DIR__.'/proofs/')) 28 | ->exit(); 29 | -------------------------------------------------------------------------------- /proofs/monoid/concat.php: -------------------------------------------------------------------------------- 1 | $a->toString() === $b->toString(); 13 | $set = Set::strings()->unicode()->map(Str::of(...)); 14 | 15 | yield properties( 16 | 'Concat properties', 17 | Monoid::properties($set, $equals), 18 | Set::of(new Concat), 19 | ); 20 | 21 | foreach (Monoid::list($set, $equals) as $property) { 22 | yield proof( 23 | 'Concat property', 24 | given($property), 25 | static fn($assert, $property) => $property->ensureHeldBy($assert, new Concat), 26 | ); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/Pair.php: -------------------------------------------------------------------------------- 1 | key; 33 | } 34 | 35 | /** 36 | * @return S 37 | */ 38 | #[\NoDiscard] 39 | public function value() 40 | { 41 | return $this->value; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /proofs/monoid/append.php: -------------------------------------------------------------------------------- 1 | $a->equals($b); 13 | $set = Set::sequence(Set::type())->map( 14 | static fn($values) => Sequence::of(...$values), 15 | ); 16 | 17 | yield properties( 18 | 'Append properties', 19 | Monoid::properties($set, $equals), 20 | Set::of(Append::of()), 21 | ); 22 | 23 | foreach (Monoid::list($set, $equals) as $property) { 24 | yield proof( 25 | 'Append property', 26 | given($property), 27 | static fn($assert, $property) => $property->ensureHeldBy($assert, Append::of()), 28 | ); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /properties/Sequence.php: -------------------------------------------------------------------------------- 1 | |Set\Provider 19 | */ 20 | public static function properties(): Set|Set\Provider 21 | { 22 | return Set\Properties::any( 23 | ...\array_map( 24 | static fn($class) => $class::any(), 25 | self::list(), 26 | ), 27 | ); 28 | } 29 | 30 | /** 31 | * @return non-empty-list> 32 | */ 33 | public static function list(): array 34 | { 35 | return [ 36 | Sequence\Windows::class, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /proofs/monoid/mergeSet.php: -------------------------------------------------------------------------------- 1 | $a->equals($b); 11 | $set = FSet::of( 12 | Set::integers()->between(0, 200), 13 | Set::integers()->between(1, 10), 14 | ); 15 | 16 | yield properties( 17 | 'MergeSet properties', 18 | Monoid::properties($set, $equals), 19 | Set::of(MergeSet::of()), 20 | ); 21 | 22 | foreach (Monoid::list($set, $equals) as $property) { 23 | yield proof( 24 | 'MergeSet property', 25 | given($property), 26 | static fn($assert, $property) => $property->ensureHeldBy($assert, MergeSet::of()), 27 | ); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/Identity/Implementation.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function map(callable $map): self; 25 | 26 | /** 27 | * @template U 28 | * 29 | * @param callable(T): Identity $map 30 | * 31 | * @return Identity 32 | */ 33 | public function flatMap(callable $map): Identity; 34 | 35 | /** 36 | * @return Sequence 37 | */ 38 | public function toSequence(): Sequence; 39 | 40 | /** 41 | * @return T 42 | */ 43 | public function unwrap(): mixed; 44 | } 45 | -------------------------------------------------------------------------------- /proofs/map.php: -------------------------------------------------------------------------------- 1 | $key) { 18 | $map = ($map)($key, $values[$index] ?? null); 19 | } 20 | 21 | $assert->true( 22 | $map->equals(Map::of( 23 | ...$map 24 | ->toSequence() 25 | ->map(static fn($pair) => [$pair->key(), $pair->value()]) 26 | ->toList(), 27 | )), 28 | ); 29 | }, 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /docs/structures/regexp.md: -------------------------------------------------------------------------------- 1 | # `RegExp` 2 | 3 | This class is here to help make sure that a string is a regular expression so you can safely type against this class. 4 | 5 | ## `::of()` 6 | 7 | This is the named cosntructor for this class. 8 | 9 | ```php 10 | RegExp::of('/foo/') instanceof RegExp; // true 11 | RegExp::of('foo'); // throws Innmind\Immutable\Exception\LogicException 12 | ``` 13 | 14 | ## `->matches()` 15 | 16 | Both examples do the same thing. 17 | 18 | ```php 19 | RegExp::of('/^a/')->matches(Str::of('abcdef')); 20 | Str::of('abcdef')->matches('/^a/'); 21 | ``` 22 | 23 | ## `->capture()` 24 | 25 | Both examples do the same thing. 26 | 27 | ```php 28 | RegExp::of('@^(?:http://)?(?P[^/]+)@i')->capture(Str::of('http://www.php.net/index.html')); 29 | Str::of('http://www.php.net/index.html')->capture('@^(?:http://)?(?P[^/]+)@i'); 30 | ``` 31 | 32 | ## `->toString()` 33 | 34 | Return the string representation of the regular expression. 35 | 36 | ```php 37 | RegExp::of('/foo/')->toString(); // '/foo/' 38 | ``` 39 | -------------------------------------------------------------------------------- /properties/Monoid.php: -------------------------------------------------------------------------------- 1 | $values 17 | * @param callable(T, T): bool $equals 18 | * 19 | * @return Set 20 | */ 21 | public static function properties(Set $values, callable $equals): Set 22 | { 23 | return Set\Properties::any(...self::list($values, $equals))->atMost(10); 24 | } 25 | 26 | /** 27 | * @template T 28 | * 29 | * @param Set $values 30 | * @param callable(T, T): bool $equals 31 | * 32 | * @return non-empty-list 33 | */ 34 | public static function list(Set $values, callable $equals): array 35 | { 36 | return [ 37 | Monoid\Identity::of($values, $equals), 38 | Monoid\Associativity::of($values, $equals), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/State/Result.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | public static function of($state, $value): self 35 | { 36 | return new self($state, $value); 37 | } 38 | 39 | /** 40 | * @return S 41 | */ 42 | public function state() 43 | { 44 | return $this->state; 45 | } 46 | 47 | /** 48 | * @return T 49 | */ 50 | public function value() 51 | { 52 | return $this->value; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Monoid/MergeSet.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | final class MergeSet implements Monoid 17 | { 18 | /** 19 | * @template C of object 20 | * 21 | * @param class-string $class 22 | * 23 | * @return self 24 | */ 25 | #[\NoDiscard] 26 | public static function of(?string $class = null): self 27 | { 28 | /** @var self */ 29 | return new self; 30 | } 31 | 32 | #[\Override] 33 | public function identity(): mixed 34 | { 35 | /** @var Set */ 36 | return Set::of(); 37 | } 38 | 39 | /** 40 | * @param Set $a 41 | * @param Set $b 42 | * 43 | * @return Set 44 | */ 45 | #[\Override] 46 | public function combine(mixed $a, mixed $b): mixed 47 | { 48 | return $a->merge($b); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 | # Testing 7 | 8 | This package provides additional sets for [`innmind/black-box`](https://packagist.org/packages/innmind/black-box) so you can more easily generate: `Set`s and `Sequence`s. 9 | 10 | For the 2 `::of()` method you can pass as last parameter an instance of `Innmind\BlackBox\Set\Intergers` to specify the range of elements to generate. By default it's between `0` and `100`, depending on the values you generate you may want to lower the upper bound to reduce the memory footprint and speed up your tests. 11 | 12 | ## `Set` 13 | 14 | ```php 15 | use Fixtures\Innmind\Immutable\Set; 16 | use Innmind\BlackBox\Set as BSet; 17 | 18 | /** @var BSet> */ 19 | $set = Set::of( 20 | BSet\Strings::any(), 21 | ); 22 | ``` 23 | 24 | ## `Sequence` 25 | 26 | ```php 27 | use Fixtures\Innmind\Immutable\Sequence; 28 | use Innmind\BlackBox\Sequence; 29 | 30 | /** @var Set> */ 31 | $set = Sequence::of( 32 | Sequence\Strings::any(), 33 | ); 34 | ``` 35 | -------------------------------------------------------------------------------- /fixtures/Sequence.php: -------------------------------------------------------------------------------- 1 | |Set\Provider $set 15 | * @param Set|Set\Provider $sizes 16 | * 17 | * @return Set> 18 | */ 19 | public static function of( 20 | Set|Set\Provider $set, 21 | Set|Set\Provider|null $sizes = null, 22 | ): Set { 23 | $sizes ??= Set::integers()->between(0, 100); 24 | 25 | return Set::compose( 26 | static fn($min, $max) => Set::sequence($set) 27 | ->between( 28 | \min($min, $max), 29 | \max($min, $max), 30 | ) 31 | ->map(static fn(array $values): Structure => Structure::of(...$values)), 32 | $sizes, 33 | $sizes, 34 | )->flatMap(static fn($sequences) => $sequences->unwrap()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Monoid/Append.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | final class Append implements Monoid 17 | { 18 | /** 19 | * @template C of object 20 | * 21 | * @param class-string $class 22 | * 23 | * @return self 24 | */ 25 | #[\NoDiscard] 26 | public static function of(?string $class = null): self 27 | { 28 | /** @var self */ 29 | return new self; 30 | } 31 | 32 | #[\Override] 33 | public function identity(): mixed 34 | { 35 | /** @var Sequence */ 36 | return Sequence::of(); 37 | } 38 | 39 | /** 40 | * @param Sequence $a 41 | * @param Sequence $b 42 | * 43 | * @return Sequence 44 | */ 45 | #[\Override] 46 | public function combine(mixed $a, mixed $b): mixed 47 | { 48 | return $a->append($b); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Identity/InMemory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class InMemory implements Implementation 15 | { 16 | /** 17 | * @param T $value 18 | */ 19 | public function __construct( 20 | private mixed $value, 21 | ) { 22 | } 23 | 24 | #[\Override] 25 | public function map(callable $map): self 26 | { 27 | /** @psalm-suppress ImpureFunctionCall */ 28 | return new self($map($this->value)); 29 | } 30 | 31 | #[\Override] 32 | public function flatMap(callable $map): Identity 33 | { 34 | /** @psalm-suppress ImpureFunctionCall */ 35 | return $map($this->value); 36 | } 37 | 38 | #[\Override] 39 | public function toSequence(): Sequence 40 | { 41 | return Sequence::of($this->value); 42 | } 43 | 44 | #[\Override] 45 | public function unwrap(): mixed 46 | { 47 | return $this->value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Monoid/MergeMap.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | final class MergeMap implements Monoid 18 | { 19 | /** 20 | * @template A of object 21 | * @template B of object 22 | * 23 | * @param class-string $key 24 | * @param class-string $value 25 | * 26 | * @return self 27 | */ 28 | #[\NoDiscard] 29 | public static function of(?string $key = null, ?string $value = null): self 30 | { 31 | /** @var self */ 32 | return new self; 33 | } 34 | 35 | #[\Override] 36 | public function identity(): mixed 37 | { 38 | /** @var Map */ 39 | return Map::of(); 40 | } 41 | 42 | /** 43 | * @param Map $a 44 | * @param Map $b 45 | * 46 | * @return Map 47 | */ 48 | #[\Override] 49 | public function combine(mixed $a, mixed $b): mixed 50 | { 51 | return $a->merge($b); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Validation/Guard.php: -------------------------------------------------------------------------------- 1 | $failures 17 | */ 18 | public function __construct( 19 | private Sequence $failures, 20 | ) { 21 | } 22 | 23 | /** 24 | * @template U 25 | * 26 | * @param callable(T): U $map 27 | * 28 | * @return self 29 | */ 30 | public function map(callable $map): self 31 | { 32 | return new self($this->failures->map($map)); 33 | } 34 | 35 | /** 36 | * @param Sequence|self $other 37 | * 38 | * @return self 39 | */ 40 | public function append(Sequence|self $other): self 41 | { 42 | if ($other instanceof self) { 43 | $other = $other->failures; 44 | } 45 | 46 | return new self( 47 | $this->failures->append($other), 48 | ); 49 | } 50 | 51 | /** 52 | * @return Sequence 53 | */ 54 | public function unwrap(): Sequence 55 | { 56 | return $this->failures; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/assets/logo.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "innmind/immutable", 3 | "type": "library", 4 | "description": "Immutable PHP primitive wrappers", 5 | "keywords": ["wrapper", "immutable"], 6 | "homepage": "http://github.com/Innmind/Immutable", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Baptiste Langlade", 11 | "email": "baptiste.langlade@hey.com" 12 | } 13 | ], 14 | "support": { 15 | "issues": "http://github.com/Innmind/Immutable/issues" 16 | }, 17 | "require": { 18 | "php": "~8.2" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Innmind\\Immutable\\": "src/", 23 | "Fixtures\\Innmind\\Immutable\\": "fixtures/", 24 | "Properties\\Innmind\\Immutable\\": "properties/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Tests\\Innmind\\Immutable\\": "tests/" 30 | } 31 | }, 32 | "require-dev": { 33 | "innmind/static-analysis": "^1.2.1", 34 | "innmind/black-box": "^6.4.1", 35 | "innmind/coding-standard": "~2.0" 36 | }, 37 | "conflict": { 38 | "innmind/black-box": "<6.0|~7.0" 39 | }, 40 | "suggest": { 41 | "innmind/black-box": "For property based testing" 42 | }, 43 | "provide": { 44 | "innmind/black-box-sets": "6.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/RegisterCleanup.php: -------------------------------------------------------------------------------- 1 | cleanup = $cleanup; 20 | } 21 | 22 | /** 23 | * @param callable(): void $cleanup 24 | */ 25 | public function __invoke(callable $cleanup): void 26 | { 27 | $this->cleanup = $cleanup; 28 | } 29 | 30 | /** 31 | * @internal 32 | * @psalm-pure 33 | */ 34 | public static function noop(): self 35 | { 36 | return new self(static fn() => null); 37 | } 38 | 39 | /** 40 | * @internal 41 | */ 42 | public function push(): self 43 | { 44 | return $this->child = self::noop(); 45 | } 46 | 47 | /** 48 | * @internal 49 | */ 50 | public function pop(): void 51 | { 52 | $this->child = null; 53 | } 54 | 55 | /** 56 | * @internal 57 | */ 58 | public function cleanup(): void 59 | { 60 | if ($this->child) { 61 | $this->child->cleanup(); 62 | } 63 | 64 | ($this->cleanup)(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /fixtures/Set.php: -------------------------------------------------------------------------------- 1 | |DataSet\Provider $set 18 | * @param DataSet|DataSet\Provider $sizes 19 | * 20 | * @return DataSet> 21 | */ 22 | public static function of( 23 | DataSet|DataSet\Provider $set, 24 | DataSet|DataSet\Provider|null $sizes = null, 25 | ): DataSet { 26 | $sizes ??= DataSet::integers()->between(0, 100); 27 | 28 | return DataSet::compose( 29 | static fn($min, $max) => DataSet::sequence($set) 30 | ->between( 31 | \min($min, $max), 32 | \max($min, $max), 33 | ) 34 | ->filter(static function(array $values): bool { 35 | // checks unicity of values 36 | return ISequence::mixed(...$values)->size() === Structure::mixed(...$values)->size(); 37 | }) 38 | ->map(static fn(array $values): Structure => Structure::of(...$values)), 39 | $sizes, 40 | $sizes, 41 | )->flatMap(static fn($sequences) => $sequences->unwrap()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /proofs/monoid/mergeMap.php: -------------------------------------------------------------------------------- 1 | $a->equals($b); 14 | $set = Set::sequence( 15 | Set::compose( 16 | static fn($key, $value): array => [$key, $value], 17 | Set::integers()->between(0, 200), 18 | Set::integers()->between(0, 200), 19 | )->randomize(), // forced to randomize as the composite will try to reuse the same key 20 | ) 21 | ->between(1, 10) 22 | ->filter(static function(array $pairs): bool { 23 | $keys = \array_column($pairs, 0); 24 | 25 | // checks unicity of values 26 | return Sequence::of(...$keys)->size() === Sequence::of(...$keys)->distinct()->size(); 27 | }) 28 | ->map(static fn($pairs) => Map::of(...$pairs)); 29 | 30 | yield properties( 31 | 'MergeMap properties', 32 | Monoid::properties($set, $equals), 33 | Set::of(MergeMap::of()), 34 | ); 35 | 36 | foreach (Monoid::list($set, $equals) as $property) { 37 | yield proof( 38 | 'MergeMap property', 39 | given($property), 40 | static fn($assert, $property) => $property->ensureHeldBy($assert, MergeMap::of()), 41 | ); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/Sequence/Sink/Continuation.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public static function of(mixed $carry): self 31 | { 32 | return new self($carry, true); 33 | } 34 | 35 | /** 36 | * @param T $carry 37 | * 38 | * @return self 39 | */ 40 | public function continue(mixed $carry): self 41 | { 42 | return new self($carry, true); 43 | } 44 | 45 | /** 46 | * @param T $carry 47 | * 48 | * @return self 49 | */ 50 | public function stop(mixed $carry): self 51 | { 52 | return new self($carry, false); 53 | } 54 | 55 | /** 56 | * @internal 57 | */ 58 | public function shouldContinue(): bool 59 | { 60 | return $this->continue; 61 | } 62 | 63 | /** 64 | * @internal 65 | * 66 | * @return T 67 | */ 68 | public function unwrap(): mixed 69 | { 70 | return $this->carry; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Identity/Lazy.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class Lazy implements Implementation 17 | { 18 | /** @var callable(): T */ 19 | private $value; 20 | 21 | /** 22 | * @param callable(): T $value 23 | */ 24 | public function __construct(callable $value) 25 | { 26 | $this->value = $value; 27 | } 28 | 29 | #[\Override] 30 | public function map(callable $map): self 31 | { 32 | $value = $this->value; 33 | 34 | /** @psalm-suppress ImpureFunctionCall */ 35 | return new self(static fn() => $map($value())); 36 | } 37 | 38 | #[\Override] 39 | public function flatMap(callable $map): Identity 40 | { 41 | $value = $this->value; 42 | 43 | /** @psalm-suppress ImpureFunctionCall */ 44 | return Identity::lazy(static fn() => $map($value())->unwrap()); 45 | } 46 | 47 | #[\Override] 48 | public function toSequence(): Sequence 49 | { 50 | $value = $this->value; 51 | 52 | return Sequence::lazy(static fn() => yield $value()); 53 | } 54 | 55 | #[\Override] 56 | public function unwrap(): mixed 57 | { 58 | /** @psalm-suppress ImpureFunctionCall */ 59 | return ($this->value)(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /proofs/monoid/arrayMerge.php: -------------------------------------------------------------------------------- 1 | $a === $b; 13 | $set = Set::sequence( 14 | Set::compose( 15 | static fn($key, $value): array => [$key, $value], 16 | Set::integers()->between(0, 200), 17 | Set::integers()->between(0, 200), 18 | )->randomize(), // forced to randomize as the composite will try to reuse the same key 19 | ) 20 | ->between(1, 10) 21 | ->filter(static function(array $pairs): bool { 22 | $keys = \array_column($pairs, 0); 23 | 24 | // checks unicity of values 25 | return Sequence::of(...$keys)->size() === Sequence::of(...$keys)->distinct()->size(); 26 | }) 27 | ->map(static fn($pairs) => \array_combine( 28 | \array_column($pairs, 0), 29 | \array_column($pairs, 1), 30 | )); 31 | 32 | yield properties( 33 | 'ArrayMerge properties', 34 | Monoid::properties($set, $equals), 35 | Set::of(new ArrayMerge), 36 | ); 37 | 38 | foreach (Monoid::list($set, $equals) as $property) { 39 | yield proof( 40 | 'ArrayMerge property', 41 | given($property), 42 | static fn($assert, $property) => $property->ensureHeldBy($assert, new ArrayMerge), 43 | ); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/Predicate/Instance.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class Instance implements Predicate 14 | { 15 | /** 16 | * @param class-string $class 17 | */ 18 | private function __construct( 19 | private string $class, 20 | ) { 21 | } 22 | 23 | #[\Override] 24 | public function __invoke(mixed $value): bool 25 | { 26 | return $value instanceof $this->class; 27 | } 28 | 29 | /** 30 | * @psalm-pure 31 | * @template T 32 | * 33 | * @param class-string $class 34 | * 35 | * @return self 36 | */ 37 | #[\NoDiscard] 38 | public static function of(string $class): self 39 | { 40 | return new self($class); 41 | } 42 | 43 | /** 44 | * @template T 45 | * 46 | * @param Predicate $predicate 47 | * 48 | * @return OrPredicate 49 | */ 50 | #[\NoDiscard] 51 | public function or(Predicate $predicate): OrPredicate 52 | { 53 | return OrPredicate::of($this, $predicate); 54 | } 55 | 56 | /** 57 | * @template T 58 | * 59 | * @param Predicate $predicate 60 | * 61 | * @return AndPredicate 62 | */ 63 | #[\NoDiscard] 64 | public function and(Predicate $predicate): AndPredicate 65 | { 66 | return AndPredicate::of($this, $predicate); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Exception/InvalidRegex.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public static function of(string $topLevel, string $subType): Maybe 19 | { 20 | if (/* $topLevel is not a valid one */) { 21 | /** @var Maybe */ 22 | return Maybe::nothing(); 23 | } 24 | 25 | return Maybe::just(new self($topLevel, $subType)); 26 | } 27 | } 28 | 29 | /** @var callable(string): Maybe $parse */ 30 | $parse = function(string $string): Maybe { 31 | // the regex only validate the form, it doesn't check the top level is a correct one 32 | $components = Str::of($string)->capture('~(?[a-z]+)/(?[a-z\-]+)~'); 33 | 34 | return Maybe::all($components->get('topLevel'), $components->get('subType')) 35 | ->flatMap(fn(Str $topLevel, Str $subType) => MediaType::of( 36 | $topLevel->toString(), 37 | $subType->toString(), 38 | )); 39 | } 40 | 41 | $parse('application/json'); // Maybe::just(new MediaType('application', 'json')) 42 | $parse(''); // Maybe::nothing() because no top level nor sub type 43 | $parse('application/'); // Maybe::nothing() because no sub type 44 | $parse('/json'); // Maybe::nothing() because no top level 45 | $parse('unknown/json'); // Maybe::nothing() because top level is not valid 46 | ``` 47 | -------------------------------------------------------------------------------- /src/Predicate/OrPredicate.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class OrPredicate implements Predicate 15 | { 16 | /** 17 | * @param Predicate $a 18 | * @param Predicate $b 19 | */ 20 | private function __construct( 21 | private Predicate $a, 22 | private Predicate $b, 23 | ) { 24 | } 25 | 26 | #[\Override] 27 | public function __invoke(mixed $value): bool 28 | { 29 | return ($this->a)($value) || ($this->b)($value); 30 | } 31 | 32 | /** 33 | * @psalm-pure 34 | * @template T 35 | * @template V 36 | * 37 | * @param Predicate $a 38 | * @param Predicate $b 39 | * 40 | * @return self 41 | */ 42 | #[\NoDiscard] 43 | public static function of(Predicate $a, Predicate $b): self 44 | { 45 | return new self($a, $b); 46 | } 47 | 48 | /** 49 | * @template C 50 | * 51 | * @param Predicate $other 52 | * 53 | * @return self 54 | */ 55 | #[\NoDiscard] 56 | public function or(Predicate $other): self 57 | { 58 | return new self($this, $other); 59 | } 60 | 61 | /** 62 | * @template C 63 | * 64 | * @param Predicate $other 65 | * 66 | * @return AndPredicate 67 | */ 68 | #[\NoDiscard] 69 | public function and(Predicate $other): AndPredicate 70 | { 71 | return AndPredicate::of($this, $other); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Fold/Implementation.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public function map(callable $map): self; 30 | 31 | /** 32 | * @template T 33 | * @template U 34 | * @template V 35 | * 36 | * @param callable(C): Fold $map 37 | * 38 | * @return Fold 39 | */ 40 | public function flatMap(callable $map): Fold; 41 | 42 | /** 43 | * @template A 44 | * 45 | * @param callable(R): A $map 46 | * 47 | * @return self 48 | */ 49 | public function mapResult(callable $map): self; 50 | 51 | /** 52 | * @template A 53 | * 54 | * @param callable(F): A $map 55 | * 56 | * @return self 57 | */ 58 | public function mapFailure(callable $map): self; 59 | 60 | /** 61 | * @return Maybe> 62 | */ 63 | public function maybe(): Maybe; 64 | 65 | /** 66 | * @template T 67 | * 68 | * @param callable(C): T $with 69 | * @param callable(R): T $result 70 | * @param callable(F): T $failure 71 | * 72 | * @return T 73 | */ 74 | public function match( 75 | callable $with, 76 | callable $result, 77 | callable $failure, 78 | ): mixed; 79 | } 80 | -------------------------------------------------------------------------------- /src/Sequence/Iterator/Primitive.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class Primitive implements \Iterator 12 | { 13 | /** 14 | * @psalm-mutation-free 15 | * 16 | * @param \ArrayIterator, T> $inner 17 | */ 18 | private function __construct( 19 | private \ArrayIterator $inner, 20 | ) { 21 | } 22 | 23 | /** 24 | * @internal 25 | * @template A 26 | * @psalm-pure 27 | * 28 | * @param list $values 29 | * 30 | * @return self 31 | */ 32 | public static function of(array $values): self 33 | { 34 | /** @psalm-suppress ImpureMethodCall */ 35 | return new self(new \ArrayIterator($values)); 36 | } 37 | 38 | /** 39 | * @return T 40 | */ 41 | #[\Override] 42 | public function current(): mixed 43 | { 44 | return $this->inner->current(); 45 | } 46 | 47 | /** 48 | * @return ?int<0, max> 49 | */ 50 | #[\Override] 51 | public function key(): ?int 52 | { 53 | return $this->inner->key(); 54 | } 55 | 56 | #[\Override] 57 | public function next(): void 58 | { 59 | $this->inner->next(); 60 | } 61 | 62 | #[\Override] 63 | public function valid(): bool 64 | { 65 | return $this->inner->valid(); 66 | } 67 | 68 | #[\Override] 69 | public function rewind(): void 70 | { 71 | $this->inner->rewind(); 72 | } 73 | 74 | public function cleanup(): void 75 | { 76 | // Do nothing to be in the same state has if the iterator was completely 77 | // iterated over. 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /docs/PHILOSOPHY.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | - toc 5 | --- 6 | 7 | # Philosophy 8 | 9 | This project was born after working with other programming languages (like [Scala](https://scala-lang.org)) and discovering [functional programming](https://en.wikipedia.org/wiki/Functional_programming). This taught me 2 things: 10 | 11 | - higher order functions on data structures 12 | - immutability 13 | 14 | ## Higher order functions 15 | 16 | PHP comes with a handful of functions to work on arrays and strings but many are missing for common use cases forcing us to implement then again and again. 17 | 18 | Examples for such cases are a `group` function on a map/set/sequence or `endsWith` on a string. `group` is a specific `reduce` function where the value computed is a map. `endsWith` is also a specific `substring` function. 19 | 20 | Higher order functions are built on top of smaller, more abstract, ones. Scala, and other languages, provides common ones on their data structures. And this project is heavily inspired from them. 21 | 22 | ## Immutability 23 | 24 | One of the core principles of functional programming is that data structures cannot change, you can only create a modified copy of them. This is extremely powerful as you can blindly give your data as a function argument with the certainty that it will not be altered. It allows you only focus on the function you are working one, without the need to worry it will have a side effect in other function, or another function triggering a side effect in yours. 25 | 26 | Another aspect of immutability is the notion of implicit state. For example in PHP an array is considered immutable because when you give it as a function argument it will create a copy of the variable. But with it comes an implicit state which is its cursor, and it is not reinitialised when a copy is created. 27 | 28 | This implicit state can generate subtle bugs in your code, that's why the structures in this project don't implement the `\Iterator` interface in order to always expose complete functions. 29 | -------------------------------------------------------------------------------- /src/Maybe/Nothing.php: -------------------------------------------------------------------------------- 1 | 16 | * @internal 17 | */ 18 | final class Nothing implements Implementation 19 | { 20 | #[\Override] 21 | public function map(callable $map): self 22 | { 23 | return $this; 24 | } 25 | 26 | #[\Override] 27 | public function flatMap(callable $map): Maybe 28 | { 29 | return Maybe::nothing(); 30 | } 31 | 32 | #[\Override] 33 | public function match(callable $just, callable $nothing) 34 | { 35 | /** @psalm-suppress ImpureFunctionCall */ 36 | return $nothing(); 37 | } 38 | 39 | #[\Override] 40 | public function otherwise(callable $otherwise): Maybe 41 | { 42 | /** @psalm-suppress ImpureFunctionCall */ 43 | return $otherwise(); 44 | } 45 | 46 | #[\Override] 47 | public function filter(callable $predicate): self 48 | { 49 | return $this; 50 | } 51 | 52 | #[\Override] 53 | public function either(): Either 54 | { 55 | return Either::left(null); 56 | } 57 | 58 | #[\Override] 59 | public function attempt(callable $error): Attempt 60 | { 61 | /** @psalm-suppress ImpureFunctionCall */ 62 | return Attempt::error($error()); 63 | } 64 | 65 | /** 66 | * @return Maybe 67 | */ 68 | #[\Override] 69 | public function memoize(): Maybe 70 | { 71 | /** @var Maybe */ 72 | return Maybe::nothing(); 73 | } 74 | 75 | #[\Override] 76 | public function toSequence(): Sequence 77 | { 78 | return Sequence::of(); 79 | } 80 | 81 | #[\Override] 82 | public function eitherWay(callable $just, callable $nothing): Maybe 83 | { 84 | /** @psalm-suppress ImpureFunctionCall */ 85 | return $nothing(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Predicate/AndPredicate.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class AndPredicate implements Predicate 15 | { 16 | /** 17 | * @param Predicate $a 18 | * @param Predicate $b 19 | */ 20 | private function __construct( 21 | private Predicate $a, 22 | private Predicate $b, 23 | ) { 24 | } 25 | 26 | #[\Override] 27 | public function __invoke(mixed $value): bool 28 | { 29 | return ($this->a)($value) && ($this->b)($value); 30 | } 31 | 32 | /** 33 | * @psalm-pure 34 | * @template T 35 | * @template V 36 | * 37 | * @param Predicate $a 38 | * @param Predicate $b 39 | * 40 | * @return self 41 | */ 42 | #[\NoDiscard] 43 | public static function of(Predicate $a, Predicate $b): self 44 | { 45 | return new self($a, $b); 46 | } 47 | 48 | /** 49 | * @template C 50 | * 51 | * @param Predicate $other 52 | * 53 | * @return OrPredicate 54 | */ 55 | #[\NoDiscard] 56 | public function or(Predicate $other): OrPredicate 57 | { 58 | /** 59 | * For some reason if using directly $this below Psalm loses the B type 60 | * @var Predicate 61 | */ 62 | $self = $this; 63 | 64 | return OrPredicate::of($self, $other); 65 | } 66 | 67 | /** 68 | * @template C 69 | * 70 | * @param Predicate $other 71 | * 72 | * @return self 73 | */ 74 | #[\NoDiscard] 75 | public function and(Predicate $other): self 76 | { 77 | /** 78 | * For some reason if using directly $this below Psalm loses the B type 79 | * @var Predicate 80 | */ 81 | $self = $this; 82 | 83 | return new self($self, $other); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Sequence/Iterator/Defer.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class Defer implements \Iterator 14 | { 15 | /** 16 | * @psalm-mutation-free 17 | * 18 | * @param Accumulate|\Generator, T> $inner 19 | */ 20 | private function __construct( 21 | private Accumulate|\Generator $inner, 22 | ) { 23 | } 24 | 25 | /** 26 | * @internal 27 | * @template A 28 | * @psalm-pure 29 | * 30 | * @param Accumulate|\Generator, A> $values 31 | * 32 | * @return self 33 | */ 34 | public static function of(Accumulate|\Generator $values): self 35 | { 36 | return new self($values); 37 | } 38 | 39 | /** 40 | * @return T 41 | */ 42 | #[\Override] 43 | public function current(): mixed 44 | { 45 | return $this->inner->current(); 46 | } 47 | 48 | /** 49 | * @return ?int<0, max> 50 | */ 51 | #[\Override] 52 | public function key(): ?int 53 | { 54 | return $this->inner->key(); 55 | } 56 | 57 | #[\Override] 58 | public function next(): void 59 | { 60 | $this->inner->next(); 61 | } 62 | 63 | #[\Override] 64 | public function valid(): bool 65 | { 66 | return $this->inner->valid(); 67 | } 68 | 69 | #[\Override] 70 | public function rewind(): void 71 | { 72 | $this->inner->rewind(); 73 | } 74 | 75 | public function cleanup(): void 76 | { 77 | if ($this->inner instanceof Accumulate) { 78 | $this->inner->cleanup(); 79 | } 80 | 81 | // If we deal with a generator then it means the intermediary Set is not 82 | // directly used. So there's a parent Accumulate consuming this 83 | // generator that will correctly cleanup its own cursor. 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Sequence/Iterator/Lazy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class Lazy implements \Iterator 14 | { 15 | /** 16 | * @psalm-mutation-free 17 | * 18 | * @param \Generator, T> $inner 19 | */ 20 | private function __construct( 21 | private \Generator $inner, 22 | private RegisterCleanup $register, 23 | ) { 24 | } 25 | 26 | /** 27 | * @internal 28 | * @template A 29 | * @psalm-pure 30 | * 31 | * @param \Closure(RegisterCleanup): \Generator, A> $generator 32 | * 33 | * @return self 34 | */ 35 | public static function of( 36 | \Closure $generator, 37 | RegisterCleanup $register, 38 | ): self { 39 | /** @psalm-suppress ImpureFunctionCall */ 40 | return new self($generator($register), $register); 41 | } 42 | 43 | /** 44 | * @return T 45 | */ 46 | #[\Override] 47 | public function current(): mixed 48 | { 49 | return $this->inner->current(); 50 | } 51 | 52 | /** 53 | * @return ?int<0, max> 54 | */ 55 | #[\Override] 56 | public function key(): ?int 57 | { 58 | return $this->inner->key(); 59 | } 60 | 61 | #[\Override] 62 | public function next(): void 63 | { 64 | $this->inner->next(); 65 | } 66 | 67 | #[\Override] 68 | public function valid(): bool 69 | { 70 | // Do not call the register cleanup as the user is expected to call a 71 | // cleanup code at the end of the generator. 72 | return $this->inner->valid(); 73 | } 74 | 75 | #[\Override] 76 | public function rewind(): void 77 | { 78 | $this->inner->rewind(); 79 | } 80 | 81 | public function cleanup(): void 82 | { 83 | $this->register->cleanup(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /properties/Monoid/Associativity.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class Associativity implements Property 18 | { 19 | /** @var T */ 20 | private mixed $a; 21 | /** @var T */ 22 | private mixed $b; 23 | /** @var T */ 24 | private mixed $c; 25 | /** @var callable(T, T): bool */ 26 | private $equals; 27 | 28 | /** 29 | * @param T $a 30 | * @param T $b 31 | * @param T $c 32 | * @param callable(T, T): bool $equals 33 | */ 34 | public function __construct(mixed $a, mixed $b, mixed $c, callable $equals) 35 | { 36 | $this->a = $a; 37 | $this->b = $b; 38 | $this->c = $c; 39 | $this->equals = $equals; 40 | } 41 | 42 | public static function any(): Set 43 | { 44 | throw new \LogicException('Use ::of() instead'); 45 | } 46 | 47 | /** 48 | * @template A 49 | * 50 | * @param Set $values 51 | * @param callable(A, A): bool $equals 52 | * 53 | * @return Set\Provider> 54 | */ 55 | public static function of(Set $values, callable $equals): Set\Provider 56 | { 57 | return Set::compose( 58 | static fn($a, $b, $c) => new self($a, $b, $c, $equals), 59 | $values, 60 | $values, 61 | $values, 62 | ); 63 | } 64 | 65 | public function applicableTo(object $monoid): bool 66 | { 67 | return true; 68 | } 69 | 70 | public function ensureHeldBy(Assert $assert, object $monoid): object 71 | { 72 | $assert->true(($this->equals)( 73 | $monoid->combine($this->a, $monoid->combine($this->b, $this->c)), 74 | $monoid->combine($monoid->combine($this->a, $this->b), $this->c), 75 | )); 76 | 77 | return $monoid; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Fold/Result.php: -------------------------------------------------------------------------------- 1 | 17 | * @psalm-immutable 18 | * @internal 19 | * @psalm-suppress DeprecatedClass 20 | */ 21 | final class Result implements Implementation 22 | { 23 | /** 24 | * @param R1 $result 25 | */ 26 | public function __construct( 27 | private mixed $result, 28 | ) { 29 | } 30 | 31 | /** 32 | * @template A 33 | * 34 | * @param callable(C1): A $map 35 | * 36 | * @return self 37 | */ 38 | #[\Override] 39 | public function map(callable $map): self 40 | { 41 | /** @var self */ 42 | return $this; 43 | } 44 | 45 | #[\Override] 46 | public function flatMap(callable $map): Fold 47 | { 48 | return Fold::result($this->result); 49 | } 50 | 51 | #[\Override] 52 | public function mapResult(callable $map): self 53 | { 54 | /** @psalm-suppress ImpureFunctionCall */ 55 | return new self($map($this->result)); 56 | } 57 | 58 | /** 59 | * @template A 60 | * 61 | * @param callable(F1): A $map 62 | * 63 | * @return self 64 | */ 65 | #[\Override] 66 | public function mapFailure(callable $map): self 67 | { 68 | /** @var self */ 69 | return $this; 70 | } 71 | 72 | /** 73 | * @return Maybe> 74 | */ 75 | #[\Override] 76 | public function maybe(): Maybe 77 | { 78 | /** @var Maybe> */ 79 | return Maybe::just(Either::right($this->result)); 80 | } 81 | 82 | #[\Override] 83 | public function match( 84 | callable $with, 85 | callable $result, 86 | callable $failure, 87 | ): mixed { 88 | /** @psalm-suppress ImpureFunctionCall */ 89 | return $result($this->result); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Maybe/Comprehension.php: -------------------------------------------------------------------------------- 1 | */ 15 | private array $rest; 16 | 17 | /** 18 | * @no-named-arguments 19 | */ 20 | private function __construct(Maybe $first, Maybe ...$rest) 21 | { 22 | $this->first = $first; 23 | $this->rest = $rest; 24 | } 25 | 26 | /** 27 | * @internal 28 | * @psalm-pure 29 | * @no-named-arguments 30 | */ 31 | public static function of(Maybe $first, Maybe ...$rest): self 32 | { 33 | return new self($first, ...$rest); 34 | } 35 | 36 | /** 37 | * @template T 38 | * 39 | * @param callable(...mixed): T $map 40 | * 41 | * @return Maybe 42 | */ 43 | #[\NoDiscard] 44 | public function map(callable $map): Maybe 45 | { 46 | return $this->collapse()->map(static fn(array $args) => $map(...$args)); 47 | } 48 | 49 | /** 50 | * @template T 51 | * 52 | * @param callable(...mixed): Maybe $map 53 | * 54 | * @return Maybe 55 | */ 56 | #[\NoDiscard] 57 | public function flatMap(callable $map): Maybe 58 | { 59 | return $this->collapse()->flatMap(static fn(array $args) => $map(...$args)); 60 | } 61 | 62 | /** 63 | * @return Maybe> 64 | */ 65 | private function collapse(): Maybe 66 | { 67 | /** 68 | * @psalm-suppress MixedArgumentTypeCoercion 69 | * @var Maybe> 70 | */ 71 | return \array_reduce( 72 | $this->rest, 73 | static fn(Maybe $carry, Maybe $maybe): Maybe => $carry->flatMap( 74 | static fn(array $args) => $maybe->map( 75 | static fn($value) => \array_merge($args, [$value]), 76 | ), 77 | ), 78 | $this->first->map(static fn($value) => [$value]), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Fold/Failure.php: -------------------------------------------------------------------------------- 1 | 17 | * @psalm-immutable 18 | * @internal 19 | * @psalm-suppress DeprecatedClass 20 | */ 21 | final class Failure implements Implementation 22 | { 23 | /** 24 | * @param F1 $failure 25 | */ 26 | public function __construct( 27 | private mixed $failure, 28 | ) { 29 | } 30 | 31 | /** 32 | * @template A 33 | * 34 | * @param callable(C1): A $map 35 | * 36 | * @return self 37 | */ 38 | #[\Override] 39 | public function map(callable $map): self 40 | { 41 | /** @var self */ 42 | return $this; 43 | } 44 | 45 | #[\Override] 46 | public function flatMap(callable $map): Fold 47 | { 48 | return Fold::fail($this->failure); 49 | } 50 | 51 | /** 52 | * @template A 53 | * 54 | * @param callable(R1): A $map 55 | * 56 | * @return self 57 | */ 58 | #[\Override] 59 | public function mapResult(callable $map): self 60 | { 61 | /** @var self */ 62 | return $this; 63 | } 64 | 65 | #[\Override] 66 | public function mapFailure(callable $map): self 67 | { 68 | /** @psalm-suppress ImpureFunctionCall */ 69 | return new self($map($this->failure)); 70 | } 71 | 72 | /** 73 | * @return Maybe> 74 | */ 75 | #[\Override] 76 | public function maybe(): Maybe 77 | { 78 | /** @var Maybe> */ 79 | return Maybe::just(Either::left($this->failure)); 80 | } 81 | 82 | #[\Override] 83 | public function match( 84 | callable $with, 85 | callable $result, 86 | callable $failure, 87 | ): mixed { 88 | /** @psalm-suppress ImpureFunctionCall */ 89 | return $failure($this->failure); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Maybe/Implementation.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function map(callable $map): self; 28 | 29 | /** 30 | * @template V 31 | * 32 | * @param callable(T): Maybe $map 33 | * 34 | * @return Maybe 35 | */ 36 | public function flatMap(callable $map): Maybe; 37 | 38 | /** 39 | * @template V 40 | * 41 | * @param callable(T): V $just 42 | * @param callable(): V $nothing 43 | * 44 | * @return V 45 | */ 46 | public function match(callable $just, callable $nothing); 47 | 48 | /** 49 | * @template V 50 | * 51 | * @param callable(): Maybe $otherwise 52 | * 53 | * @return Maybe 54 | */ 55 | public function otherwise(callable $otherwise): Maybe; 56 | 57 | /** 58 | * @param callable(T): bool $predicate 59 | * 60 | * @return self 61 | */ 62 | public function filter(callable $predicate): self; 63 | 64 | /** 65 | * @return Either 66 | */ 67 | public function either(): Either; 68 | 69 | /** 70 | * @param callable(): \Throwable $error 71 | * 72 | * @return Attempt 73 | */ 74 | public function attempt(callable $error): Attempt; 75 | 76 | /** 77 | * @return Maybe 78 | */ 79 | public function memoize(): Maybe; 80 | 81 | /** 82 | * @return Sequence 83 | */ 84 | public function toSequence(): Sequence; 85 | 86 | /** 87 | * @template V 88 | * 89 | * @param callable(T): Maybe $just 90 | * @param callable(): Maybe $nothing 91 | * 92 | * @return Maybe 93 | */ 94 | public function eitherWay(callable $just, callable $nothing): Maybe; 95 | } 96 | -------------------------------------------------------------------------------- /properties/Monoid/Identity.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class Identity implements Property 18 | { 19 | /** @var T */ 20 | private mixed $value; 21 | /** @var callable(T, T): bool */ 22 | private $equals; 23 | 24 | /** 25 | * @param T $value 26 | * @param callable(T, T): bool $equals 27 | */ 28 | public function __construct(mixed $value, callable $equals) 29 | { 30 | $this->value = $value; 31 | $this->equals = $equals; 32 | } 33 | 34 | public static function any(): Set 35 | { 36 | throw new \LogicException('Use ::of() instead'); 37 | } 38 | 39 | /** 40 | * @template A 41 | * 42 | * @param Set $values 43 | * @param callable(A, A): bool $equals 44 | * 45 | * @return Set> 46 | */ 47 | public static function of(Set $values, callable $equals): Set 48 | { 49 | return $values->map( 50 | static fn($value) => new self($value, $equals), 51 | ); 52 | } 53 | 54 | public function applicableTo(object $monoid): bool 55 | { 56 | return true; 57 | } 58 | 59 | public function ensureHeldBy(Assert $assert, object $monoid): object 60 | { 61 | $assert->true(($this->equals)( 62 | $monoid->identity(), 63 | $monoid->identity(), 64 | )); 65 | $assert->true(($this->equals)( 66 | $this->value, 67 | $monoid->combine($monoid->identity(), $this->value), 68 | )); 69 | $assert->true(($this->equals)( 70 | $this->value, 71 | $monoid->combine($this->value, $monoid->identity()), 72 | )); 73 | // make sure the identiy is not altered after using a concrete value 74 | $assert->true(($this->equals)( 75 | $monoid->identity(), 76 | $monoid->identity(), 77 | )); 78 | 79 | return $monoid; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/State.php: -------------------------------------------------------------------------------- 1 | */ 18 | private $run; 19 | 20 | /** 21 | * @param callable(S): Result $run 22 | */ 23 | private function __construct(callable $run) 24 | { 25 | $this->run = $run; 26 | } 27 | 28 | /** 29 | * @psalm-pure 30 | * @template A 31 | * @template B 32 | * 33 | * @param callable(A): Result $run 34 | * 35 | * @return self 36 | */ 37 | #[\NoDiscard] 38 | public static function of(callable $run): self 39 | { 40 | return new self($run); 41 | } 42 | 43 | /** 44 | * @template U 45 | * 46 | * @param callable(T): U $map 47 | * 48 | * @return self 49 | */ 50 | #[\NoDiscard] 51 | public function map(callable $map): self 52 | { 53 | $run = $this->run; 54 | 55 | return new self(static function(mixed $state) use ($run, $map) { 56 | /** @var S $state */ 57 | $result = $run($state); 58 | 59 | return Result::of($result->state(), $map($result->value())); 60 | }); 61 | } 62 | 63 | /** 64 | * @template A 65 | * 66 | * @param callable(T): self $map 67 | * 68 | * @return self 69 | */ 70 | #[\NoDiscard] 71 | public function flatMap(callable $map): self 72 | { 73 | $run = $this->run; 74 | 75 | return new self(static function(mixed $state) use ($run, $map) { 76 | /** @var S $state */ 77 | $result = $run($state); 78 | 79 | return $map($result->value())->run($result->state()); 80 | }); 81 | } 82 | 83 | /** 84 | * @param S $state 85 | * 86 | * @return Result 87 | */ 88 | #[\NoDiscard] 89 | public function run($state): Result 90 | { 91 | /** @psalm-suppress ImpureFunctionCall */ 92 | return ($this->run)($state); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /docs/MONOIDS.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | - toc 5 | --- 6 | 7 | # Monoids 8 | 9 | Monoids describe a way to combine two values of a given type. A monoid contains an identity value that when combined with another value doesn't change its value. The combine operation has to be associative meaning `combine(a, combine(b, c))` is the same as `combine(combine(a, b), c)`. 10 | 11 | A simple monoid is an addition because adding `0` (the identity value) to any other integer won't change the value and `add(1, add(2, 3))` is the the same result as `add(add(1, 2), 3)` (both return 6). 12 | 13 | This library comes with a few monoids: 14 | 15 | - `Innmind\Immutable\Monoid\Concat` to append 2 instances of `Innmind\Immutable\Str` together 16 | - `Innmind\Immutable\Monoid\Append` to append 2 instances of `Innmind\Immutable\Sequence` together 17 | - `Innmind\Immutable\Monoid\MergeSet` to append 2 instances of `Innmind\Immutable\Set` together 18 | - `Innmind\Immutable\Monoid\MergeMap` to append 2 instances of `Innmind\Immutable\Map` together 19 | - `Innmind\Immutable\Monoid\ArrayMerge` to append 2 arrays together (keys are preserved) 20 | 21 | ## Create your own 22 | 23 | To make sure your own monoid follows the laws this library comes with properties you can use (via [`innmind/black-box`](https://github.com/Innmind/BlackBox/)) in your test like so: 24 | 25 | ```php 26 | use Innmind\BlackBox\Set; 27 | use Properties\Innmind\Immutable\Monoid; 28 | 29 | return static function() { 30 | $equals = static fn($a, $b) => /* this callable is the way to check that 2 values are equal */; 31 | // this Set must generate values that are of the type your monoid understands 32 | $set = /* an instance of Set */; 33 | 34 | yield properties( 35 | 'YourMonoid properties', 36 | Monoid::properties($set, $equals), 37 | Set\Elements::of(new YourMonoid), 38 | ); 39 | 40 | foreach (Monoid::list($set, $equals) as $property) { 41 | yield proof( 42 | 'YourMonoid property', 43 | given($property), 44 | static fn($assert, $property) => $property->ensureHeldBy($assert, new YourMonoid), 45 | ); 46 | } 47 | }; 48 | ``` 49 | 50 | You can take a look at the [proofs](https://github.com/Innmind/Immutable/tree/master/proofs/monoid) for this package monoids to better understand how thiw works. 51 | -------------------------------------------------------------------------------- /docs/structures/state.md: -------------------------------------------------------------------------------- 1 | # `State` 2 | 3 | ??? warning "Deprecated" 4 | `State` is deprecated and will be removed in the next major release. 5 | 6 | The `State` monad allows you to build a set of pure steps to compute a new state. Since the initial state is given when all the steps are built it means that all steps are lazy, this use function composition (so everything is kept in memory). 7 | 8 | The state and value can be of any type. 9 | 10 | ## `::of()` 11 | 12 | ```php 13 | use Innmind\Immutable\{ 14 | State, 15 | State\Result, 16 | }; 17 | 18 | /** @var State */ 19 | $state = State::of(function(array $logs) { 20 | return Result::of($logs, 0); 21 | }); 22 | ``` 23 | 24 | ## `->map()` 25 | 26 | This method will modify the value without affecting the currently held state. 27 | 28 | ```php 29 | use Innmind\Immutable\{ 30 | State, 31 | State\Result, 32 | }; 33 | 34 | /** @var State */ 35 | $state = State::of(function(array $logs) { 36 | return Result::of($logs, 0); 37 | }); 38 | 39 | $state = $state->map(fn($value) => $value + 1); 40 | ``` 41 | 42 | ## `->flatMap()` 43 | 44 | This method allows you to modify both state and values. 45 | 46 | ```php 47 | use Innmind\Immutable\{ 48 | State, 49 | State\Result, 50 | }; 51 | 52 | /** @var State */ 53 | $state = State::of(function(array $logs) { 54 | return Result::of($logs, 0); 55 | }); 56 | 57 | $state = $state->flatMap(fn($value) => State::of(function(array $logs) use ($value) { 58 | $value++; 59 | 60 | return Result::of( 61 | \array_merge($logs, "The new value is $value"), 62 | $value, 63 | ); 64 | })); 65 | ``` 66 | 67 | ## `->run()` 68 | 69 | This is the only place where you can run the steps to compute the new state. 70 | 71 | ```php 72 | use Innmind\Immutable\{ 73 | State, 74 | State\Result, 75 | }; 76 | 77 | /** @var State */ 78 | $result = State::of(function(array $logs) { 79 | return Result::of($logs, 0); 80 | }) 81 | ->map(fn($value) => $value + 1) 82 | ->flatMap(fn($value) => State::of(function(array $logs) use ($value) { 83 | $value++; 84 | 85 | return Result::of( 86 | \array_merge($logs, "The new value is $value"), 87 | $value, 88 | ); 89 | })) 90 | ->run([]); 91 | 92 | $result->state(); // ['The new value is 2'] 93 | $result->value(); // 2 94 | ``` 95 | -------------------------------------------------------------------------------- /src/Fold/With.php: -------------------------------------------------------------------------------- 1 | 17 | * @psalm-immutable 18 | * @internal 19 | * @psalm-suppress DeprecatedClass 20 | */ 21 | final class With implements Implementation 22 | { 23 | /** 24 | * @param C1 $with 25 | */ 26 | public function __construct( 27 | private mixed $with, 28 | ) { 29 | } 30 | 31 | /** 32 | * @template A 33 | * 34 | * @param callable(C1): A $map 35 | * 36 | * @return self 37 | */ 38 | #[\Override] 39 | public function map(callable $map): self 40 | { 41 | /** 42 | * @psalm-suppress ImpureFunctionCall 43 | * @var self 44 | */ 45 | return new self($map($this->with)); 46 | } 47 | 48 | #[\Override] 49 | public function flatMap(callable $map): Fold 50 | { 51 | /** @psalm-suppress ImpureFunctionCall */ 52 | return $map($this->with); 53 | } 54 | 55 | /** 56 | * @template A 57 | * 58 | * @param callable(R1): A $map 59 | * 60 | * @return self 61 | */ 62 | #[\Override] 63 | public function mapResult(callable $map): self 64 | { 65 | /** @var self */ 66 | return $this; 67 | } 68 | 69 | /** 70 | * @template A 71 | * 72 | * @param callable(F1): A $map 73 | * 74 | * @return self 75 | */ 76 | #[\Override] 77 | public function mapFailure(callable $map): self 78 | { 79 | /** @var self */ 80 | return $this; 81 | } 82 | 83 | /** 84 | * @return Maybe> 85 | */ 86 | #[\Override] 87 | public function maybe(): Maybe 88 | { 89 | /** @var Maybe> */ 90 | return Maybe::nothing(); 91 | } 92 | 93 | #[\Override] 94 | public function match( 95 | callable $with, 96 | callable $result, 97 | callable $failure, 98 | ): mixed { 99 | /** @psalm-suppress ImpureFunctionCall */ 100 | return $with($this->with); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Maybe/Just.php: -------------------------------------------------------------------------------- 1 | 16 | * @psalm-immutable 17 | * @internal 18 | */ 19 | final class Just implements Implementation 20 | { 21 | /** 22 | * @param V $value 23 | */ 24 | public function __construct( 25 | private mixed $value, 26 | ) { 27 | } 28 | 29 | #[\Override] 30 | public function map(callable $map): self 31 | { 32 | /** @psalm-suppress ImpureFunctionCall */ 33 | return new self($map($this->value)); 34 | } 35 | 36 | #[\Override] 37 | public function flatMap(callable $map): Maybe 38 | { 39 | /** @psalm-suppress ImpureFunctionCall */ 40 | return $map($this->value); 41 | } 42 | 43 | #[\Override] 44 | public function match(callable $just, callable $nothing) 45 | { 46 | /** @psalm-suppress ImpureFunctionCall */ 47 | return $just($this->value); 48 | } 49 | 50 | #[\Override] 51 | public function otherwise(callable $otherwise): Maybe 52 | { 53 | return Maybe::just($this->value); 54 | } 55 | 56 | #[\Override] 57 | public function filter(callable $predicate): Implementation 58 | { 59 | /** @psalm-suppress ImpureFunctionCall */ 60 | if ($predicate($this->value) === true) { 61 | return $this; 62 | } 63 | 64 | return new Nothing; 65 | } 66 | 67 | #[\Override] 68 | public function either(): Either 69 | { 70 | return Either::right($this->value); 71 | } 72 | 73 | #[\Override] 74 | public function attempt(callable $error): Attempt 75 | { 76 | return Attempt::result($this->value); 77 | } 78 | 79 | /** 80 | * @return Maybe 81 | */ 82 | #[\Override] 83 | public function memoize(): Maybe 84 | { 85 | return Maybe::just($this->value); 86 | } 87 | 88 | #[\Override] 89 | public function toSequence(): Sequence 90 | { 91 | return Sequence::of($this->value); 92 | } 93 | 94 | #[\Override] 95 | public function eitherWay(callable $just, callable $nothing): Maybe 96 | { 97 | /** @psalm-suppress ImpureFunctionCall */ 98 | return $just($this->value); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/RegExp.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 27 | } 28 | 29 | /** 30 | * @psalm-pure 31 | */ 32 | #[\NoDiscard] 33 | public static function of(string $pattern): self 34 | { 35 | return new self($pattern); 36 | } 37 | 38 | /** 39 | * @throws InvalidRegex 40 | */ 41 | #[\NoDiscard] 42 | public function matches(Str $string): bool 43 | { 44 | /** @psalm-suppress ArgumentTypeCoercion */ 45 | $value = \preg_match($this->pattern, $string->toString()); 46 | 47 | if ($value === false) { 48 | /** @psalm-suppress ImpureFunctionCall */ 49 | throw new InvalidRegex('', \preg_last_error()); 50 | } 51 | 52 | return (bool) $value; 53 | } 54 | 55 | /** 56 | * @throws InvalidRegex 57 | * 58 | * @return Map 59 | */ 60 | #[\NoDiscard] 61 | public function capture(Str $string): Map 62 | { 63 | $matches = []; 64 | /** @psalm-suppress ArgumentTypeCoercion */ 65 | $value = \preg_match($this->pattern, $string->toString(), $matches); 66 | 67 | if ($value === false) { 68 | /** @psalm-suppress ImpureFunctionCall */ 69 | throw new InvalidRegex('', \preg_last_error()); 70 | } 71 | 72 | /** @var Map */ 73 | $map = Map::of(); 74 | 75 | foreach ($matches as $key => $match) { 76 | /** @psalm-suppress RedundantCast Don't trust the types of preg_match */ 77 | $map = ($map)( 78 | $key, 79 | Str::of( 80 | (string) $match, 81 | $string->encoding(), 82 | ), 83 | ); 84 | } 85 | 86 | return $map; 87 | } 88 | 89 | #[\NoDiscard] 90 | public function toString(): string 91 | { 92 | return $this->pattern; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Either/Implementation.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function map(callable $map): self; 28 | 29 | /** 30 | * @template A 31 | * @template B 32 | * 33 | * @param callable(R): Either $map 34 | * 35 | * @return Either 36 | */ 37 | public function flatMap(callable $map): Either; 38 | 39 | /** 40 | * @template T 41 | * 42 | * @param callable(L): T $map 43 | * 44 | * @return self 45 | */ 46 | public function leftMap(callable $map): self; 47 | 48 | /** 49 | * @template T 50 | * 51 | * @param callable(R): T $right 52 | * @param callable(L): T $left 53 | * 54 | * @return T 55 | */ 56 | public function match(callable $right, callable $left); 57 | 58 | /** 59 | * @template A 60 | * @template B 61 | * 62 | * @param callable(L): Either $otherwise 63 | * 64 | * @return Either 65 | */ 66 | public function otherwise(callable $otherwise): Either; 67 | 68 | /** 69 | * @template A 70 | * 71 | * @param callable(R): bool $predicate 72 | * @param callable(): A $otherwise 73 | * 74 | * @return self 75 | */ 76 | public function filter(callable $predicate, callable $otherwise): self; 77 | 78 | /** 79 | * @return Maybe 80 | */ 81 | public function maybe(): Maybe; 82 | 83 | /** 84 | * @param callable(L): \Throwable $error 85 | * 86 | * @return Attempt 87 | */ 88 | public function attempt(callable $error): Attempt; 89 | 90 | /** 91 | * @return Either 92 | */ 93 | public function memoize(): Either; 94 | 95 | /** 96 | * @return self 97 | */ 98 | public function flip(): self; 99 | 100 | /** 101 | * @template A 102 | * @template B 103 | * 104 | * @param callable(R): Either $right 105 | * @param callable(L): Either $left 106 | * 107 | * @return Either 108 | */ 109 | public function eitherWay(callable $right, callable $left): Either; 110 | } 111 | -------------------------------------------------------------------------------- /src/Sequence/Iterator.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class Iterator implements \Iterator 20 | { 21 | /** 22 | * @psalm-mutation-free 23 | * 24 | * @param Lazy|Defer|Primitive $inner 25 | */ 26 | private function __construct( 27 | private Lazy|Defer|Primitive $inner, 28 | ) { 29 | } 30 | 31 | /** 32 | * @internal 33 | * @template A 34 | * @psalm-pure 35 | * 36 | * @param list $values 37 | * 38 | * @return self 39 | */ 40 | public static function primitive(array $values): self 41 | { 42 | return new self(Primitive::of($values)); 43 | } 44 | 45 | /** 46 | * @internal 47 | * @template A 48 | * @psalm-pure 49 | * 50 | * @param Accumulate|\Generator, A> $values 51 | * 52 | * @return self 53 | */ 54 | public static function defer(Accumulate|\Generator $values): self 55 | { 56 | return new self(Defer::of($values)); 57 | } 58 | 59 | /** 60 | * @internal 61 | * @template A 62 | * @psalm-pure 63 | * 64 | * @param \Closure(RegisterCleanup): \Generator, A> $generator 65 | * 66 | * @return self 67 | */ 68 | public static function lazy( 69 | \Closure $generator, 70 | RegisterCleanup $register, 71 | ): self { 72 | return new self(Lazy::of($generator, $register)); 73 | } 74 | 75 | /** 76 | * @return T 77 | */ 78 | #[\Override] 79 | public function current(): mixed 80 | { 81 | return $this->inner->current(); 82 | } 83 | 84 | /** 85 | * @return ?int<0, max> 86 | */ 87 | #[\Override] 88 | public function key(): ?int 89 | { 90 | return $this->inner->key(); 91 | } 92 | 93 | #[\Override] 94 | public function next(): void 95 | { 96 | $this->inner->next(); 97 | } 98 | 99 | #[\Override] 100 | public function valid(): bool 101 | { 102 | return $this->inner->valid(); 103 | } 104 | 105 | #[\Override] 106 | public function rewind(): void 107 | { 108 | $this->inner->rewind(); 109 | } 110 | 111 | public function cleanup(): void 112 | { 113 | $this->inner->cleanup(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /properties/Sequence/Windows.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class Windows implements Property 17 | { 18 | private function __construct( 19 | private int $size, 20 | ) { 21 | } 22 | 23 | public static function any(): Set 24 | { 25 | // Upper bound is 100 to avoid having too large windows as it would 26 | // reduce the probability to create multiple windows. 27 | return Set::integers() 28 | ->between(1, 100) 29 | ->map(static fn($size) => new self($size)); 30 | } 31 | 32 | public function applicableTo(object $systemUnderTest): bool 33 | { 34 | return true; 35 | } 36 | 37 | public function ensureHeldBy(Assert $assert, object $systemUnderTest): object 38 | { 39 | $systemUnderTest 40 | ->windows($this->size) 41 | ->foreach( 42 | fn($window) => $assert 43 | ->number($window->size()) 44 | ->int() 45 | ->lessThanOrEqual($this->size), 46 | ); 47 | 48 | if ($systemUnderTest->size() >= $this->size) { 49 | $systemUnderTest 50 | ->windows($this->size) 51 | ->foreach(fn($window) => $assert->same( 52 | $this->size, 53 | $window->size(), 54 | )); 55 | } 56 | 57 | $end = new \stdClass; 58 | $assert->same( 59 | $systemUnderTest->toList(), 60 | $systemUnderTest 61 | ->add($end) 62 | ->windows($this->size) 63 | ->flatMap(static fn($window) => match ($window->contains($end)) { 64 | true => $window->dropEnd(1), 65 | false => $window->take(1), 66 | }) 67 | ->exclude(static fn($value) => $value === $end) 68 | ->toList(), 69 | ); 70 | $assert->same( 71 | $systemUnderTest->toList(), 72 | $systemUnderTest 73 | ->prepend(Sequence::of($end)) 74 | ->windows($this->size) 75 | ->flatMap(static fn($window) => match ($window->contains($end)) { 76 | true => $window->drop(1), 77 | false => $window->takeEnd(1), 78 | }) 79 | ->exclude(static fn($value) => $value === $end) 80 | ->toList(), 81 | ); 82 | 83 | return $systemUnderTest; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Either/Left.php: -------------------------------------------------------------------------------- 1 | 16 | * @psalm-immutable 17 | * @internal 18 | */ 19 | final class Left implements Implementation 20 | { 21 | /** 22 | * @param L1 $value 23 | */ 24 | public function __construct( 25 | private mixed $value, 26 | ) { 27 | } 28 | 29 | /** 30 | * @template T 31 | * 32 | * @param callable(R1): T $map 33 | * 34 | * @return self 35 | */ 36 | #[\Override] 37 | public function map(callable $map): self 38 | { 39 | /** @var self */ 40 | return $this; 41 | } 42 | 43 | #[\Override] 44 | public function flatMap(callable $map): Either 45 | { 46 | return Either::left($this->value); 47 | } 48 | 49 | #[\Override] 50 | public function leftMap(callable $map): self 51 | { 52 | /** @psalm-suppress ImpureFunctionCall */ 53 | return new self($map($this->value)); 54 | } 55 | 56 | #[\Override] 57 | public function match(callable $right, callable $left) 58 | { 59 | /** @psalm-suppress ImpureFunctionCall */ 60 | return $left($this->value); 61 | } 62 | 63 | #[\Override] 64 | public function otherwise(callable $otherwise): Either 65 | { 66 | /** @psalm-suppress ImpureFunctionCall */ 67 | return $otherwise($this->value); 68 | } 69 | 70 | #[\Override] 71 | public function filter(callable $predicate, callable $otherwise): self 72 | { 73 | return $this; 74 | } 75 | 76 | #[\Override] 77 | public function maybe(): Maybe 78 | { 79 | return Maybe::nothing(); 80 | } 81 | 82 | #[\Override] 83 | public function attempt(callable $error): Attempt 84 | { 85 | /** @psalm-suppress ImpureFunctionCall */ 86 | return Attempt::error($error($this->value)); 87 | } 88 | 89 | /** 90 | * @return Either 91 | */ 92 | #[\Override] 93 | public function memoize(): Either 94 | { 95 | return Either::left($this->value); 96 | } 97 | 98 | #[\Override] 99 | public function flip(): Implementation 100 | { 101 | return new Right($this->value); 102 | } 103 | 104 | #[\Override] 105 | public function eitherWay(callable $right, callable $left): Either 106 | { 107 | /** @psalm-suppress ImpureFunctionCall */ 108 | return $left($this->value); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Attempt/Result.php: -------------------------------------------------------------------------------- 1 | 14 | * @psalm-immutable 15 | * @internal 16 | */ 17 | final class Result implements Implementation 18 | { 19 | /** 20 | * @param R1 $value 21 | */ 22 | public function __construct( 23 | private mixed $value, 24 | ) { 25 | } 26 | 27 | #[\Override] 28 | public function map(callable $map): self 29 | { 30 | /** @psalm-suppress ImpureFunctionCall */ 31 | return new self($map($this->value)); 32 | } 33 | 34 | #[\Override] 35 | public function flatMap( 36 | callable $map, 37 | callable $exfiltrate, 38 | ): Implementation { 39 | /** @psalm-suppress ImpureFunctionCall */ 40 | return $exfiltrate($map($this->value)); 41 | } 42 | 43 | #[\Override] 44 | public function guard( 45 | callable $map, 46 | callable $exfiltrate, 47 | ): Implementation { 48 | /** @psalm-suppress ImpureFunctionCall */ 49 | return $exfiltrate($map($this->value))->guardError(); 50 | } 51 | 52 | #[\Override] 53 | public function guardError(): self 54 | { 55 | return $this; 56 | } 57 | 58 | #[\Override] 59 | public function match(callable $result, callable $error) 60 | { 61 | /** @psalm-suppress ImpureFunctionCall */ 62 | return $result($this->value); 63 | } 64 | 65 | #[\Override] 66 | public function mapError(callable $map): self 67 | { 68 | return $this; 69 | } 70 | 71 | #[\Override] 72 | public function recover( 73 | callable $recover, 74 | callable $exfiltrate, 75 | ): self { 76 | return $this; 77 | } 78 | 79 | #[\Override] 80 | public function xrecover( 81 | callable $recover, 82 | callable $exfiltrate, 83 | ): self { 84 | return $this; 85 | } 86 | 87 | #[\Override] 88 | public function maybe(): Maybe 89 | { 90 | return Maybe::just($this->value); 91 | } 92 | 93 | #[\Override] 94 | public function either(): Either 95 | { 96 | return Either::right($this->value); 97 | } 98 | 99 | #[\Override] 100 | public function memoize(callable $exfiltrate): self 101 | { 102 | return $this; 103 | } 104 | 105 | #[\Override] 106 | public function eitherWay( 107 | callable $result, 108 | callable $error, 109 | callable $exfiltrate, 110 | ): Implementation { 111 | /** @psalm-suppress ImpureFunctionCall */ 112 | return $exfiltrate($result($this->value)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Either/Right.php: -------------------------------------------------------------------------------- 1 | 16 | * @psalm-immutable 17 | * @internal 18 | */ 19 | final class Right implements Implementation 20 | { 21 | /** 22 | * @param R1 $value 23 | */ 24 | public function __construct( 25 | private mixed $value, 26 | ) { 27 | } 28 | 29 | #[\Override] 30 | public function map(callable $map): self 31 | { 32 | /** @psalm-suppress ImpureFunctionCall */ 33 | return new self($map($this->value)); 34 | } 35 | 36 | #[\Override] 37 | public function flatMap(callable $map): Either 38 | { 39 | /** @psalm-suppress ImpureFunctionCall */ 40 | return $map($this->value); 41 | } 42 | 43 | /** 44 | * @template T 45 | * 46 | * @param callable(L1): T $map 47 | * 48 | * @return self 49 | */ 50 | #[\Override] 51 | public function leftMap(callable $map): self 52 | { 53 | /** @var self */ 54 | return $this; 55 | } 56 | 57 | #[\Override] 58 | public function match(callable $right, callable $left) 59 | { 60 | /** @psalm-suppress ImpureFunctionCall */ 61 | return $right($this->value); 62 | } 63 | 64 | #[\Override] 65 | public function otherwise(callable $otherwise): Either 66 | { 67 | return Either::right($this->value); 68 | } 69 | 70 | #[\Override] 71 | public function filter(callable $predicate, callable $otherwise): Implementation 72 | { 73 | /** @psalm-suppress ImpureFunctionCall */ 74 | if ($predicate($this->value) === true) { 75 | return $this; 76 | } 77 | 78 | /** @psalm-suppress ImpureFunctionCall */ 79 | return new Left($otherwise()); 80 | } 81 | 82 | #[\Override] 83 | public function maybe(): Maybe 84 | { 85 | return Maybe::just($this->value); 86 | } 87 | 88 | #[\Override] 89 | public function attempt(callable $error): Attempt 90 | { 91 | return Attempt::result($this->value); 92 | } 93 | 94 | /** 95 | * @return Either 96 | */ 97 | #[\Override] 98 | public function memoize(): Either 99 | { 100 | return Either::right($this->value); 101 | } 102 | 103 | #[\Override] 104 | public function flip(): Implementation 105 | { 106 | return new Left($this->value); 107 | } 108 | 109 | #[\Override] 110 | public function eitherWay(callable $right, callable $left): Either 111 | { 112 | /** @psalm-suppress ImpureFunctionCall */ 113 | return $right($this->value); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Identity.php: -------------------------------------------------------------------------------- 1 | $implementation 21 | */ 22 | private function __construct( 23 | private Implementation $implementation, 24 | ) { 25 | } 26 | 27 | /** 28 | * @psalm-pure 29 | * @template A 30 | * 31 | * @param A $value 32 | * 33 | * @return self 34 | */ 35 | #[\NoDiscard] 36 | public static function of(mixed $value): self 37 | { 38 | return new self(new InMemory($value)); 39 | } 40 | 41 | /** 42 | * When using a lazy computation all transformations via map and flatMap 43 | * will be applied when calling unwrap. Each call to unwrap will call again 44 | * all transformations. 45 | * 46 | * @psalm-pure 47 | * @template A 48 | * 49 | * @param callable(): A $value 50 | * 51 | * @return self 52 | */ 53 | #[\NoDiscard] 54 | public static function lazy(callable $value): self 55 | { 56 | return new self(new Lazy($value)); 57 | } 58 | 59 | /** 60 | * When using a deferred computation all transformations via map and flatMap 61 | * will be applied when calling unwrap. The value is computed once and all 62 | * calls to unwrap will return the same value. 63 | * 64 | * @psalm-pure 65 | * @template A 66 | * 67 | * @param callable(): A $value 68 | * 69 | * @return self 70 | */ 71 | #[\NoDiscard] 72 | public static function defer(callable $value): self 73 | { 74 | return new self(new Defer($value)); 75 | } 76 | 77 | /** 78 | * @template U 79 | * 80 | * @param callable(T): U $map 81 | * 82 | * @return self 83 | */ 84 | #[\NoDiscard] 85 | public function map(callable $map): self 86 | { 87 | return new self($this->implementation->map($map)); 88 | } 89 | 90 | /** 91 | * @template U 92 | * 93 | * @param callable(T): self $map 94 | * 95 | * @return self 96 | */ 97 | #[\NoDiscard] 98 | public function flatMap(callable $map): self 99 | { 100 | return $this->implementation->flatMap($map); 101 | } 102 | 103 | /** 104 | * @return Sequence 105 | */ 106 | #[\NoDiscard] 107 | public function toSequence(): Sequence 108 | { 109 | return $this->implementation->toSequence(); 110 | } 111 | 112 | /** 113 | * @return T 114 | */ 115 | #[\NoDiscard] 116 | public function unwrap(): mixed 117 | { 118 | return $this->implementation->unwrap(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Validation/Implementation.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function map(callable $map): self; 28 | 29 | /** 30 | * @template T 31 | * @template V 32 | * 33 | * @param callable(S): Validation $map 34 | * @param pure-callable(Validation): self $exfiltrate 35 | * 36 | * @return self 37 | */ 38 | public function flatMap(callable $map, callable $exfiltrate): self; 39 | 40 | /** 41 | * @template T 42 | * @template V 43 | * 44 | * @param callable(S): Validation $map 45 | * @param pure-callable(Validation): self $exfiltrate 46 | * 47 | * @return self 48 | */ 49 | public function guard(callable $map, callable $exfiltrate): self; 50 | 51 | /** 52 | * @return self 53 | */ 54 | public function guardFailures(): self; 55 | 56 | /** 57 | * @template T 58 | * 59 | * @param callable(F): T $map 60 | * 61 | * @return self 62 | */ 63 | public function mapFailures(callable $map): self; 64 | 65 | /** 66 | * @template T 67 | * @template V 68 | * 69 | * @param callable(Sequence): Validation $map 70 | * 71 | * @return Validation 72 | */ 73 | public function otherwise(callable $map): Validation; 74 | 75 | /** 76 | * @template T 77 | * @template V 78 | * 79 | * @param callable(Sequence): Validation $map 80 | * @param callable(self): Validation $wrap 81 | * 82 | * @return Validation 83 | */ 84 | public function xotherwise( 85 | callable $map, 86 | callable $wrap, 87 | ): Validation; 88 | 89 | /** 90 | * @template A 91 | * @template T 92 | * 93 | * @param self $other 94 | * @param callable(S, A): T $fold 95 | * 96 | * @return self 97 | */ 98 | public function and(self $other, callable $fold): self; 99 | 100 | /** 101 | * @template T 102 | * 103 | * @param callable(S): T $success 104 | * @param callable(Sequence): T $failure 105 | * 106 | * @return T 107 | */ 108 | public function match(callable $success, callable $failure); 109 | 110 | /** 111 | * @return Maybe 112 | */ 113 | public function maybe(): Maybe; 114 | 115 | /** 116 | * @return Either, S> 117 | */ 118 | public function either(): Either; 119 | } 120 | -------------------------------------------------------------------------------- /src/Identity/Defer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class Defer implements Implementation 17 | { 18 | /** @var callable(): T */ 19 | private $value; 20 | private bool $loaded = false; 21 | /** @var ?T */ 22 | private mixed $computed = null; 23 | 24 | /** 25 | * @param callable(): T $value 26 | */ 27 | public function __construct(callable $value) 28 | { 29 | $this->value = $value; 30 | } 31 | 32 | #[\Override] 33 | public function map(callable $map): self 34 | { 35 | $captured = $this->capture(); 36 | 37 | /** 38 | * @psalm-suppress ImpureFunctionCall 39 | * @psalm-suppress MixedArgument 40 | */ 41 | return new self(static fn() => $map(self::detonate($captured))); 42 | } 43 | 44 | #[\Override] 45 | public function flatMap(callable $map): Identity 46 | { 47 | $captured = $this->capture(); 48 | 49 | /** 50 | * @psalm-suppress ImpureFunctionCall 51 | * @psalm-suppress MixedArgument 52 | */ 53 | return Identity::defer(static fn() => $map(self::detonate($captured))->unwrap()); 54 | } 55 | 56 | #[\Override] 57 | public function toSequence(): Sequence 58 | { 59 | $captured = $this->capture(); 60 | 61 | /** @psalm-suppress ImpureFunctionCall */ 62 | return Sequence::defer((static fn() => yield self::detonate($captured))()); 63 | } 64 | 65 | #[\Override] 66 | public function unwrap(): mixed 67 | { 68 | if ($this->loaded) { 69 | /** @var T */ 70 | return $this->computed; 71 | } 72 | 73 | /** 74 | * @psalm-suppress InaccessibleProperty 75 | * @psalm-suppress ImpureFunctionCall 76 | */ 77 | $this->computed = ($this->value)(); 78 | /** @psalm-suppress InaccessibleProperty */ 79 | $this->loaded = true; 80 | 81 | return $this->computed; 82 | } 83 | 84 | /** 85 | * @return array{\WeakReference>, callable(): T} 86 | */ 87 | private function capture(): array 88 | { 89 | /** @psalm-suppress ImpureMethodCall */ 90 | return [ 91 | \WeakReference::create($this), 92 | $this->value, 93 | ]; 94 | } 95 | 96 | /** 97 | * @template V 98 | * 99 | * @param array{\WeakReference>, callable(): V} $captured 100 | * 101 | * @return V 102 | */ 103 | private static function detonate(array $captured): mixed 104 | { 105 | [$ref, $value] = $captured; 106 | $self = $ref->get(); 107 | 108 | if (\is_null($self)) { 109 | return $value(); 110 | } 111 | 112 | return $self->unwrap(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /docs/use-cases/lazy-file.md: -------------------------------------------------------------------------------- 1 | # How to read a file 2 | 3 | This example below will show you how to build complex pipelines to read files without ever loading the whole file in memory, allowing you to read any file's size. 4 | 5 | This example below looks for a user in a csv file with a score above `100`. 6 | 7 | ```php 8 | use Innmind\Immutable\{ 9 | Sequence, 10 | Str, 11 | }; 12 | 13 | $lines = Sequence::lazy(function(callable $registerCleanup) { 14 | $handle = \fopen('users.csv', 'r'); 15 | $registerCleanup(fn() => \fclose($handle)); 16 | 17 | while (!\feof($handle)) { 18 | yield (string) \fgets($handle); 19 | } 20 | 21 | \fclose($handle); 22 | }); 23 | /** @var Maybe */ 24 | $user = $lines 25 | ->map(fn(string $line) => Str::of($line)) 26 | ->map(fn(Str $line) => $line->trim()) 27 | ->filter(fn(Str $line) => !$line->empty()) 28 | ->filter(fn(Str $line) => $line->contains(',')) 29 | ->map(fn(Str $line) => $line->split(',')) 30 | ->map(fn(Sequence $columns) => User::of($columns)) // ficticious class 31 | ->find(fn(User $user) => $user->score() > 100); 32 | ``` 33 | 34 | The final `$user` variable is an instance of `Maybe` because the user may or may not exist. 35 | 36 | If a user is found before the end of the file the sequence will stop reading the file and call the function passed to `$registerCleanup` allowing you to close the file handle properly. 37 | 38 | Since this is a lazy sequence the file is iterated over only when trying to extract a concrete value, in this case via `->find()`. This means that even though the pipeline contains multiple steps the file is read only once. This decoupling between reading the file and building a pipeline to compute a value allows you to split the construction of the pipeline across multiple layers in your application without worrying about performance. 39 | 40 | The other advantage of this technique is that it allows to read files that may not fit in memory. 41 | 42 | ## Merging multiple files in a single pipeline 43 | 44 | The lazyness described above still works when you combine multiple lazy sequences. 45 | 46 | ```php 47 | $openFile = fn(string $name) => function(callable $registerCleanup) use ($name) { 48 | $handle = \fopen($name, 'r'); 49 | $registerCleanup(fn() => \fclose($handle)); 50 | 51 | while (!\feof($handle)) { 52 | yield (string) \fgets($handle); 53 | } 54 | 55 | \fclose($handle); 56 | }; 57 | 58 | $users = Sequence::lazy($openFile('users1.csv')) 59 | ->append(Sequence::lazy($openFile('users2.csv'))) 60 | ->append(Sequence::lazy($openFile('users3.csv'))); 61 | 62 | /** @var callable(Sequence): Maybe $findUser */ 63 | $findUser = function(Sequence $users): Maybe { 64 | // todo build a pipeline to find a user 65 | }; 66 | 67 | $user = $findUser($users); 68 | ``` 69 | 70 | Here we create a sequence that will sequentially read the 3 files `users1.csv`, `users2.csv` and `users3.csv` but the `$findUser` function is not aware of where the data comes from. This composition will open `users2.csv` only if the user is not found in `users1.csv`. 71 | 72 | In this example the 3 sources are all files but you could mix the sources with different generators, for example you could combine from a file with another one coming from a database. 73 | -------------------------------------------------------------------------------- /src/Attempt/Implementation.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | public function map(callable $map): self; 27 | 28 | /** 29 | * @template U 30 | * 31 | * @param callable(T): Attempt $map 32 | * @param pure-callable(Attempt): self $exfiltrate 33 | * 34 | * @return self 35 | */ 36 | public function flatMap( 37 | callable $map, 38 | callable $exfiltrate, 39 | ): self; 40 | 41 | /** 42 | * @template U 43 | * 44 | * @param callable(T): Attempt $map 45 | * @param pure-callable(Attempt): self $exfiltrate 46 | * 47 | * @return self 48 | */ 49 | public function guard( 50 | callable $map, 51 | callable $exfiltrate, 52 | ): self; 53 | 54 | /** 55 | * @return self 56 | */ 57 | public function guardError(): self; 58 | 59 | /** 60 | * @template U 61 | * 62 | * @param callable(T): U $result 63 | * @param callable(\Throwable): U $error 64 | * 65 | * @return U 66 | */ 67 | public function match(callable $result, callable $error); 68 | 69 | /** 70 | * @param callable(\Throwable): \Throwable $map 71 | * 72 | * @return self 73 | */ 74 | public function mapError(callable $map): self; 75 | 76 | /** 77 | * @template U 78 | * 79 | * @param callable(\Throwable): Attempt $recover 80 | * @param pure-callable(Attempt): self $exfiltrate 81 | * 82 | * @return self 83 | */ 84 | public function recover( 85 | callable $recover, 86 | callable $exfiltrate, 87 | ): self; 88 | 89 | /** 90 | * @template U 91 | * 92 | * @param callable(\Throwable): Attempt $recover 93 | * @param pure-callable(Attempt): self $exfiltrate 94 | * 95 | * @return self 96 | */ 97 | public function xrecover( 98 | callable $recover, 99 | callable $exfiltrate, 100 | ): self; 101 | 102 | /** 103 | * @return Maybe 104 | */ 105 | public function maybe(): Maybe; 106 | 107 | /** 108 | * @return Either<\Throwable, T> 109 | */ 110 | public function either(): Either; 111 | 112 | /** 113 | * @param pure-callable(Attempt): self $exfiltrate 114 | * 115 | * @return self 116 | */ 117 | public function memoize(callable $exfiltrate): self; 118 | 119 | /** 120 | * @template V 121 | * 122 | * @param callable(T): Attempt $result 123 | * @param callable(\Throwable): Attempt $error 124 | * @param pure-callable(Attempt): self $exfiltrate 125 | * 126 | * @return self 127 | */ 128 | public function eitherWay( 129 | callable $result, 130 | callable $error, 131 | callable $exfiltrate, 132 | ): self; 133 | } 134 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Innmind/Immutable 2 | repo_name: Innmind/Immutable 3 | 4 | nav: 5 | - Getting Started: README.md 6 | - Philosophy: PHILOSOPHY.md 7 | - Structures: 8 | - structures/index.md 9 | - Sequence: structures/sequence.md 10 | - Set: structures/set.md 11 | - Map: structures/map.md 12 | - Str: structures/str.md 13 | - RegExp: structures/regexp.md 14 | - Maybe: structures/maybe.md 15 | - Either: structures/either.md 16 | - Attempt: structures/attempt.md 17 | - Validation: structures/validation.md 18 | - Identity: structures/identity.md 19 | - State: structures/state.md 20 | - Fold: structures/fold.md 21 | - Monoids: MONOIDS.md 22 | - Use cases: 23 | - use-cases/index.md 24 | - How to read a file: use-cases/lazy-file.md 25 | - Parsing strings: use-cases/parsing.md 26 | - Testing: testing.md 27 | 28 | theme: 29 | name: material 30 | logo: assets/logo.svg 31 | favicon: assets/favicon.png 32 | font: false 33 | features: 34 | - content.code.copy 35 | - content.code.annotate 36 | - navigation.tracking 37 | - navigation.tabs 38 | - navigation.tabs.sticky 39 | - navigation.sections 40 | - navigation.expand 41 | - navigation.indexes 42 | - navigation.top 43 | - navigation.footer 44 | - search.suggest 45 | - search.highlight 46 | - content.action.edit 47 | palette: 48 | # Palette toggle for automatic mode 49 | - media: "(prefers-color-scheme)" 50 | toggle: 51 | icon: material/brightness-auto 52 | name: Switch to light mode 53 | primary: blue 54 | accent: deep orange 55 | # Palette toggle for light mode 56 | - media: "(prefers-color-scheme: light)" 57 | scheme: default 58 | toggle: 59 | icon: material/brightness-7 60 | name: Switch to dark mode 61 | primary: blue 62 | accent: deep orange 63 | # Palette toggle for dark mode 64 | - media: "(prefers-color-scheme: dark)" 65 | scheme: slate 66 | toggle: 67 | icon: material/brightness-4 68 | name: Switch to system preference 69 | primary: blue 70 | accent: deep orange 71 | 72 | markdown_extensions: 73 | - pymdownx.highlight: 74 | anchor_linenums: true 75 | line_spans: __span 76 | pygments_lang_class: true 77 | extend_pygments_lang: 78 | - name: php 79 | lang: php 80 | options: 81 | startinline: true 82 | - pymdownx.inlinehilite 83 | - pymdownx.snippets 84 | - attr_list 85 | - md_in_html 86 | - pymdownx.superfences 87 | - abbr 88 | - admonition 89 | - pymdownx.details: 90 | - pymdownx.tabbed: 91 | alternate_style: true 92 | - toc: 93 | permalink: true 94 | - footnotes 95 | - pymdownx.emoji: 96 | emoji_index: !!python/name:material.extensions.emoji.twemoji 97 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 98 | 99 | extra_css: 100 | - assets/stylesheets/extra.css 101 | 102 | plugins: 103 | - search 104 | - privacy 105 | 106 | extra: 107 | social: 108 | - icon: fontawesome/brands/github 109 | link: https://github.com/Innmind/immutable 110 | - icon: fontawesome/brands/x-twitter 111 | link: https://twitter.com/Baptouuuu 112 | -------------------------------------------------------------------------------- /proofs/either.php: -------------------------------------------------------------------------------- 1 | filter(static fn($value) => !\is_null($value))), 11 | static function($assert, $value) { 12 | $loaded = false; 13 | $either = Either::defer(static fn() => Either::right($value)) 14 | ->flatMap(static function() use ($value, &$loaded) { 15 | return Either::defer(static function() use ($value, &$loaded) { 16 | $loaded = true; 17 | 18 | return Either::right($value); 19 | }); 20 | }); 21 | 22 | $assert->false($loaded); 23 | $either->memoize(); 24 | $assert->true($loaded); 25 | }, 26 | ); 27 | 28 | yield proof( 29 | 'Either->attempt()', 30 | given( 31 | Set::type(), 32 | Set::type(), 33 | ), 34 | static function($assert, $right, $left) { 35 | $assert->same( 36 | $right, 37 | Either::right($right) 38 | ->attempt(static fn() => new Exception) 39 | ->unwrap(), 40 | ); 41 | 42 | $expected = new Exception; 43 | $assert->same( 44 | $expected, 45 | Either::left($left) 46 | ->attempt(static function($value) use ($assert, $left, $expected) { 47 | $assert->same($left, $value); 48 | 49 | return $expected; 50 | }) 51 | ->match( 52 | static fn() => null, 53 | static fn($error) => $error, 54 | ), 55 | ); 56 | }, 57 | ); 58 | 59 | yield proof( 60 | 'Either::defer()->attempt()', 61 | given( 62 | Set::type(), 63 | Set::type(), 64 | ), 65 | static function($assert, $right, $left) { 66 | $loaded = false; 67 | $attempt = Either::defer(static function() use (&$loaded, $right) { 68 | $loaded = true; 69 | 70 | return Either::right($right); 71 | })->attempt(static fn() => new Exception); 72 | 73 | $assert->false($loaded); 74 | $assert->same( 75 | $right, 76 | $attempt->unwrap(), 77 | ); 78 | $assert->true($loaded); 79 | 80 | $expected = new Exception; 81 | $loaded = false; 82 | $attempt = Either::defer(static function() use (&$loaded, $left) { 83 | $loaded = true; 84 | 85 | return Either::left($left); 86 | })->attempt(static function($value) use ($assert, $left, $expected) { 87 | $assert->same($left, $value); 88 | 89 | return $expected; 90 | }); 91 | 92 | $assert->false($loaded); 93 | $assert->same( 94 | $expected, 95 | $attempt->match( 96 | static fn() => null, 97 | static fn($error) => $error, 98 | ), 99 | ); 100 | $assert->true($loaded); 101 | }, 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /src/Fold.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | #[\NoDiscard] 39 | public static function with(mixed $value): self 40 | { 41 | return new self(new With($value)); 42 | } 43 | 44 | /** 45 | * @psalm-pure 46 | * 47 | * @template T 48 | * @template U 49 | * @template V 50 | * 51 | * @param U $result 52 | * 53 | * @return self 54 | */ 55 | #[\NoDiscard] 56 | public static function result(mixed $result): self 57 | { 58 | return new self(new Result($result)); 59 | } 60 | 61 | /** 62 | * @psalm-pure 63 | * 64 | * @template T 65 | * @template U 66 | * @template V 67 | * 68 | * @param T $failure 69 | * 70 | * @return self 71 | */ 72 | #[\NoDiscard] 73 | public static function fail(mixed $failure): self 74 | { 75 | return new self(new Failure($failure)); 76 | } 77 | 78 | /** 79 | * @template A 80 | * 81 | * @param callable(C): A $map 82 | * 83 | * @return self 84 | */ 85 | #[\NoDiscard] 86 | public function map(callable $map): self 87 | { 88 | return new self($this->fold->map($map)); 89 | } 90 | 91 | /** 92 | * @template T 93 | * @template U 94 | * @template V 95 | * 96 | * @param callable(C): self $map 97 | * 98 | * @return self 99 | */ 100 | #[\NoDiscard] 101 | public function flatMap(callable $map): self 102 | { 103 | return $this->fold->flatMap($map); 104 | } 105 | 106 | /** 107 | * @template A 108 | * 109 | * @param callable(R): A $map 110 | * 111 | * @return self 112 | */ 113 | #[\NoDiscard] 114 | public function mapResult(callable $map): self 115 | { 116 | return new self($this->fold->mapResult($map)); 117 | } 118 | 119 | /** 120 | * @template A 121 | * 122 | * @param callable(F): A $map 123 | * 124 | * @return self 125 | */ 126 | #[\NoDiscard] 127 | public function mapFailure(callable $map): self 128 | { 129 | return new self($this->fold->mapFailure($map)); 130 | } 131 | 132 | /** 133 | * @return Maybe> 134 | */ 135 | #[\NoDiscard] 136 | public function maybe(): Maybe 137 | { 138 | return $this->fold->maybe(); 139 | } 140 | 141 | /** 142 | * @template T 143 | * 144 | * @param callable(C): T $with 145 | * @param callable(R): T $result 146 | * @param callable(F): T $failure 147 | * 148 | * @return T 149 | */ 150 | #[\NoDiscard] 151 | public function match( 152 | callable $with, 153 | callable $result, 154 | callable $failure, 155 | ): mixed { 156 | return $this->fold->match($with, $result, $failure); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /docs/structures/identity.md: -------------------------------------------------------------------------------- 1 | # `Identity` 2 | 3 | This is the simplest monad there is. It's a simple wrapper around a value to allow chaining calls on this value. 4 | 5 | Let's say you have a string you want to camelize, here's how you'd do it: 6 | 7 | === "Identity" 8 | ```php 9 | $value = Identity::of('some value to camelize') 10 | ->map(fn($string) => \explode(' ', $string)) 11 | ->map(fn($parts) => \array_map( 12 | \ucfirst(...), 13 | $parts, 14 | )) 15 | ->map(fn($parts) => \implode('', $parts)) 16 | ->map(\lcfirst(...)) 17 | ->unwrap(); 18 | 19 | echo $value; // outputs "someValueToCamelize" 20 | ``` 21 | 22 | === "Imperative" 23 | ```php 24 | $string = 'some value to camelize'; 25 | $parts = \explode(' ', $string); 26 | $parts = \array_map( 27 | \ucfirst(...), 28 | $parts, 29 | ); 30 | $string = \implode('', $parts); 31 | $value = \lcfirst($string); 32 | 33 | echo $value; // outputs "someValueToCamelize" 34 | ``` 35 | 36 | === "Pyramid of doom" 37 | ```php 38 | $value = \lcfirst( 39 | \implode( 40 | '', 41 | \array_map( 42 | \ucfirst(...), 43 | \explode( 44 | ' ', 45 | 'some value to camelize', 46 | ), 47 | ), 48 | ), 49 | ); 50 | 51 | echo $value; // outputs "someValueToCamelize" 52 | ``` 53 | 54 | !!! abstract "" 55 | In the end this monad does not provide any behaviour, it's a different way to write and read your code. 56 | 57 | ## Lazy computation 58 | 59 | By default the `Identity` apply each transformation when `map` or `flatMap` is called. But you can defer the application of the transformations to when the `unwrap` method is called. This can be useful when you're not sure the computed value will be really used. 60 | 61 | Instead of using `Identity::of()` you'd do: 62 | 63 | ```php 64 | $value = Identity::defer(static fn() => 'some value'); //(1) 65 | // or 66 | $value = Identity::lazy(static fn() => 'some value'); 67 | // then you can use the identity as before (see above) 68 | ``` 69 | 70 | 1. Here the value is a string but you can use whatever type you want. 71 | 72 | The difference between `lazy` and `defer` is that the first one will recompute the underlying value each time the `unwrap` method is called while the other one will compute it once and then always return the same value. 73 | 74 | ## Wrapping the underlying value in a `Sequence` 75 | 76 | This monad has a `toSequence` method that will create a new [`Sequence`](sequence.md) containing the underlying value. 77 | 78 | Both examples do the same: 79 | 80 | === "Declarative" 81 | ```php 82 | $value = Identity::of('some value') 83 | ->toSequence(); 84 | ``` 85 | 86 | === "Imperative" 87 | ```php 88 | $value = Sequence::of('some value'); 89 | ``` 90 | 91 | On the surface this seems to not be very useful, but it becomes interesting when the identity is lazy or deferred. The laziness is propagated to the sequence. 92 | 93 | Both examples do the same: 94 | 95 | === "Declarative" 96 | ```php 97 | $value = Identity::lazy(static fn() => 'some value') 98 | ->toSequence(); 99 | ``` 100 | 101 | === "Imperative" 102 | ```php 103 | $value = Sequence::lazy(static fn() => yield 'some value'); 104 | ``` 105 | 106 | This combined to the [`Sequence::toIdentity()`](sequence.md#-toidentity) allows you to chain and compose sequences without having to be aware if the source sequence is lazy or not. 107 | -------------------------------------------------------------------------------- /proofs/predicate.php: -------------------------------------------------------------------------------- 1 | true( 13 | Instance::of(Countable::class) 14 | ->or(Instance::of(stdClass::class)) 15 | ($array), 16 | ); 17 | $assert->true( 18 | Instance::of(stdClass::class) 19 | ->or(Instance::of(Countable::class)) 20 | ($array), 21 | ); 22 | $assert->false( 23 | Instance::of(Throwable::class) 24 | ->or(Instance::of(Unknown::class)) 25 | ($array), 26 | ); 27 | }, 28 | ); 29 | 30 | yield test( 31 | 'Or predicate is chainable', 32 | static function($assert) { 33 | $array = new SplFixedArray; 34 | 35 | $assert->true( 36 | Instance::of(Unknown::class) 37 | ->or(Instance::of(stdClass::class)) 38 | ->or(Instance::of(Countable::class)) 39 | ($array), 40 | ); 41 | $assert->true( 42 | Instance::of(Unknown::class) 43 | ->or(Instance::of(Countable::class)) 44 | ->or(Instance::of(stdClass::class)) 45 | ($array), 46 | ); 47 | $assert->true( 48 | Instance::of(Countable::class) 49 | ->or(Instance::of(stdClass::class)) 50 | ->or(Instance::of(Unknown::class)) 51 | ($array), 52 | ); 53 | $assert->false( 54 | Instance::of(Throwable::class) 55 | ->or(Instance::of(Unknown::class)) 56 | ->or(Instance::of(Unknown2::class)) 57 | ($array), 58 | ); 59 | }, 60 | ); 61 | 62 | yield test( 63 | 'And predicate', 64 | static function($assert) { 65 | $array = new SplFixedArray; 66 | 67 | $assert->true( 68 | Instance::of(Countable::class) 69 | ->and(Instance::of(Traversable::class)) 70 | ($array), 71 | ); 72 | $assert->false( 73 | Instance::of(Throwable::class) 74 | ->and(Instance::of(Countable::class)) 75 | ($array), 76 | ); 77 | $assert->false( 78 | Instance::of(Countable::class) 79 | ->and(Instance::of(Throwable::class)) 80 | ($array), 81 | ); 82 | }, 83 | ); 84 | 85 | yield test( 86 | 'And predicate is chainable', 87 | static function($assert) { 88 | $array = new SplFixedArray; 89 | 90 | $assert->true( 91 | Instance::of(Countable::class) 92 | ->and(Instance::of(Traversable::class)) 93 | ->and(Instance::of(JsonSerializable::class)) 94 | ($array), 95 | ); 96 | $assert->false( 97 | Instance::of(Traversable::class) 98 | ->and(Instance::of(Throwable::class)) 99 | ->and(Instance::of(Countable::class)) 100 | ($array), 101 | ); 102 | $assert->false( 103 | Instance::of(Countable::class) 104 | ->and(Instance::of(Throwable::class)) 105 | ($array), 106 | ); 107 | }, 108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /src/Attempt/Error.php: -------------------------------------------------------------------------------- 1 | 14 | * @psalm-immutable 15 | * @internal 16 | */ 17 | final class Error implements Implementation 18 | { 19 | public function __construct( 20 | private \Throwable|Guard $value, 21 | ) { 22 | } 23 | 24 | /** 25 | * @template T 26 | * 27 | * @param callable(R1): T $map 28 | * 29 | * @return self 30 | */ 31 | #[\Override] 32 | public function map(callable $map): self 33 | { 34 | /** @var self */ 35 | return $this; 36 | } 37 | 38 | #[\Override] 39 | public function flatMap( 40 | callable $map, 41 | callable $exfiltrate, 42 | ): self { 43 | return $this; 44 | } 45 | 46 | #[\Override] 47 | public function guard( 48 | callable $map, 49 | callable $exfiltrate, 50 | ): self { 51 | return $this; 52 | } 53 | 54 | #[\Override] 55 | public function guardError(): self 56 | { 57 | if ($this->value instanceof Guard) { 58 | return $this; 59 | } 60 | 61 | return new self(new Guard($this->value)); 62 | } 63 | 64 | #[\Override] 65 | public function match(callable $result, callable $error) 66 | { 67 | if ($this->value instanceof Guard) { 68 | /** @psalm-suppress ImpureFunctionCall */ 69 | return $error($this->value->unwrap()); 70 | } 71 | 72 | /** @psalm-suppress ImpureFunctionCall */ 73 | return $error($this->value); 74 | } 75 | 76 | #[\Override] 77 | public function mapError(callable $map): self 78 | { 79 | if ($this->value instanceof Guard) { 80 | /** @psalm-suppress ImpureFunctionCall */ 81 | return new self(new Guard( 82 | $map($this->value->unwrap()), 83 | )); 84 | } 85 | 86 | /** @psalm-suppress ImpureFunctionCall */ 87 | return new self($map($this->value)); 88 | } 89 | 90 | #[\Override] 91 | public function recover( 92 | callable $recover, 93 | callable $exfiltrate, 94 | ): Implementation { 95 | if ($this->value instanceof Guard) { 96 | /** @psalm-suppress ImpureFunctionCall */ 97 | return $exfiltrate($recover($this->value->unwrap())); 98 | } 99 | 100 | /** @psalm-suppress ImpureFunctionCall */ 101 | return $exfiltrate($recover($this->value)); 102 | } 103 | 104 | #[\Override] 105 | public function xrecover( 106 | callable $recover, 107 | callable $exfiltrate, 108 | ): Implementation { 109 | if ($this->value instanceof Guard) { 110 | return $this; 111 | } 112 | 113 | /** @psalm-suppress ImpureFunctionCall */ 114 | return $exfiltrate($recover($this->value)); 115 | } 116 | 117 | #[\Override] 118 | public function maybe(): Maybe 119 | { 120 | return Maybe::nothing(); 121 | } 122 | 123 | #[\Override] 124 | public function either(): Either 125 | { 126 | if ($this->value instanceof Guard) { 127 | return Either::left($this->value->unwrap()); 128 | } 129 | 130 | return Either::left($this->value); 131 | } 132 | 133 | #[\Override] 134 | public function memoize(callable $exfiltrate): self 135 | { 136 | return $this; 137 | } 138 | 139 | #[\Override] 140 | public function eitherWay( 141 | callable $result, 142 | callable $error, 143 | callable $exfiltrate, 144 | ): Implementation { 145 | if ($this->value instanceof Guard) { 146 | /** @psalm-suppress ImpureFunctionCall */ 147 | return $exfiltrate($error($this->value->unwrap())); 148 | } 149 | 150 | /** @psalm-suppress ImpureFunctionCall */ 151 | return $exfiltrate($error($this->value)); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /proofs/maybe.php: -------------------------------------------------------------------------------- 1 | map(static fn() => $value2); 27 | 28 | $assert->same( 29 | $value2, 30 | $m2->match( 31 | static fn($value) => $value, 32 | static fn() => null, 33 | ), 34 | ); 35 | $assert->not()->throws( 36 | static fn() => $assert->same( 37 | $value1, 38 | $m1->match( 39 | static fn($value) => $value, 40 | static fn() => null, 41 | ), 42 | ), 43 | ); 44 | }, 45 | ); 46 | 47 | yield proof( 48 | 'Maybe::memoize() any composition', 49 | given(Set::type()->filter(static fn($value) => !\is_null($value))), 50 | static function($assert, $value) { 51 | $loaded = false; 52 | $maybe = Maybe::defer(static fn() => Maybe::just($value)) 53 | ->flatMap(static function() use ($value, &$loaded) { 54 | return Maybe::defer(static function() use ($value, &$loaded) { 55 | $loaded = true; 56 | 57 | return Maybe::just($value); 58 | }); 59 | }); 60 | 61 | $assert->false($loaded); 62 | $maybe->memoize(); 63 | $assert->true($loaded); 64 | }, 65 | ); 66 | 67 | yield proof( 68 | 'Maybe->attempt()', 69 | given(Set::type()), 70 | static function($assert, $value) { 71 | $assert->same( 72 | $value, 73 | Maybe::just($value) 74 | ->attempt(static fn() => new Exception) 75 | ->unwrap(), 76 | ); 77 | 78 | $expected = new Exception; 79 | $assert->same( 80 | $expected, 81 | Maybe::nothing() 82 | ->attempt(static fn() => $expected) 83 | ->match( 84 | static fn() => null, 85 | static fn($error) => $error, 86 | ), 87 | ); 88 | }, 89 | ); 90 | 91 | yield proof( 92 | 'Maybe::defer()->attempt()', 93 | given(Set::type()), 94 | static function($assert, $value) { 95 | $loaded = false; 96 | $attempt = Maybe::defer(static function() use (&$loaded, $value) { 97 | $loaded = true; 98 | 99 | return Maybe::just($value); 100 | })->attempt(static fn() => new Exception); 101 | 102 | $assert->false($loaded); 103 | $assert->same( 104 | $value, 105 | $attempt->unwrap(), 106 | ); 107 | $assert->true($loaded); 108 | 109 | $expected = new Exception; 110 | $loaded = false; 111 | $attempt = Maybe::defer(static function() use (&$loaded) { 112 | $loaded = true; 113 | 114 | return Maybe::nothing(); 115 | })->attempt(static fn() => $expected); 116 | 117 | $assert->false($loaded); 118 | $assert->same( 119 | $expected, 120 | $attempt->match( 121 | static fn() => null, 122 | static fn($error) => $error, 123 | ), 124 | ); 125 | $assert->true($loaded); 126 | }, 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /src/Map/Implementation.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function __invoke($key, $value): self; 32 | 33 | /** 34 | * @return int<0, max> 35 | */ 36 | public function size(): int; 37 | 38 | /** 39 | * Return the element with the given key 40 | * 41 | * @param T $key 42 | * 43 | * @return Maybe 44 | */ 45 | public function get($key): Maybe; 46 | 47 | /** 48 | * Check if there is an element for the given key 49 | * 50 | * @param T $key 51 | */ 52 | public function contains($key): bool; 53 | 54 | /** 55 | * Return an empty map given the same given type 56 | * 57 | * @return self 58 | */ 59 | public function clear(): self; 60 | 61 | /** 62 | * Check if the two maps are equal 63 | * 64 | * @param self $map 65 | */ 66 | public function equals(self $map): bool; 67 | 68 | /** 69 | * Filter the map based on the given predicate 70 | * 71 | * @param callable(T, S): bool $predicate 72 | * 73 | * @return self 74 | */ 75 | public function filter(callable $predicate): self; 76 | 77 | /** 78 | * Run the given function for each element of the map 79 | * 80 | * @param callable(T, S): void $function 81 | */ 82 | public function foreach(callable $function): SideEffect; 83 | 84 | /** 85 | * Return a new map of pairs' sequences grouped by keys determined with the given 86 | * discriminator function 87 | * 88 | * @template D 89 | * 90 | * @param callable(T, S): D $discriminator 91 | * 92 | * @return Map> 93 | */ 94 | public function groupBy(callable $discriminator): Map; 95 | 96 | /** 97 | * Return all keys 98 | * 99 | * @return Set 100 | */ 101 | public function keys(): Set; 102 | 103 | /** 104 | * Return all values 105 | * 106 | * @return Sequence 107 | */ 108 | public function values(): Sequence; 109 | 110 | /** 111 | * Apply the given function on all elements and return a new map 112 | * 113 | * @template B 114 | * 115 | * @param callable(T, S): B $function 116 | * 117 | * @return self 118 | */ 119 | public function map(callable $function): self; 120 | 121 | /** 122 | * Remove the element with the given key 123 | * 124 | * @param T $key 125 | * 126 | * @return self 127 | */ 128 | public function remove($key): self; 129 | 130 | /** 131 | * Create a new map by combining both maps 132 | * 133 | * @param self $map 134 | * 135 | * @return self 136 | */ 137 | public function merge(self $map): self; 138 | 139 | /** 140 | * Return a map of 2 maps partitioned according to the given predicate 141 | * 142 | * @param callable(T, S): bool $predicate 143 | * 144 | * @return Map> 145 | */ 146 | public function partition(callable $predicate): Map; 147 | 148 | /** 149 | * Reduce the map to a single value 150 | * 151 | * @template I 152 | * @template R 153 | * 154 | * @param I $carry 155 | * @param callable(I|R, T, S): R $reducer 156 | * 157 | * @return I|R 158 | */ 159 | public function reduce($carry, callable $reducer); 160 | 161 | public function empty(): bool; 162 | 163 | /** 164 | * @param callable(T, S): bool $predicate 165 | * 166 | * @return Maybe> 167 | */ 168 | public function find(callable $predicate): Maybe; 169 | 170 | /** 171 | * @return Sequence> 172 | */ 173 | public function toSequence(): Sequence; 174 | } 175 | -------------------------------------------------------------------------------- /src/Maybe/Defer.php: -------------------------------------------------------------------------------- 1 | 16 | * @psalm-immutable 17 | * @internal 18 | */ 19 | final class Defer implements Implementation 20 | { 21 | /** @var callable(): Maybe */ 22 | private $deferred; 23 | /** @var ?Maybe */ 24 | private ?Maybe $value = null; 25 | 26 | /** 27 | * @param callable(): Maybe $deferred 28 | */ 29 | public function __construct(callable $deferred) 30 | { 31 | $this->deferred = $deferred; 32 | } 33 | 34 | #[\Override] 35 | public function map(callable $map): self 36 | { 37 | $captured = $this->capture(); 38 | 39 | return new self(static fn() => self::detonate($captured)->map($map)); 40 | } 41 | 42 | #[\Override] 43 | public function flatMap(callable $map): Maybe 44 | { 45 | $captured = $this->capture(); 46 | 47 | return Maybe::defer(static fn() => self::detonate($captured)->flatMap($map)); 48 | } 49 | 50 | #[\Override] 51 | public function match(callable $just, callable $nothing) 52 | { 53 | return $this->unwrap()->match($just, $nothing); 54 | } 55 | 56 | #[\Override] 57 | public function otherwise(callable $otherwise): Maybe 58 | { 59 | $captured = $this->capture(); 60 | 61 | return Maybe::defer(static fn() => self::detonate($captured)->otherwise($otherwise)); 62 | } 63 | 64 | #[\Override] 65 | public function filter(callable $predicate): Implementation 66 | { 67 | $captured = $this->capture(); 68 | 69 | return new self(static fn() => self::detonate($captured)->filter($predicate)); 70 | } 71 | 72 | #[\Override] 73 | public function either(): Either 74 | { 75 | $captured = $this->capture(); 76 | 77 | return Either::defer(static fn() => self::detonate($captured)->either()); 78 | } 79 | 80 | #[\Override] 81 | public function attempt(callable $error): Attempt 82 | { 83 | $captured = $this->capture(); 84 | 85 | return Attempt::defer(static fn() => self::detonate($captured)->attempt($error)); 86 | } 87 | 88 | /** 89 | * @return Maybe 90 | */ 91 | #[\Override] 92 | public function memoize(): Maybe 93 | { 94 | return $this->unwrap(); 95 | } 96 | 97 | #[\Override] 98 | public function toSequence(): Sequence 99 | { 100 | $captured = $this->capture(); 101 | 102 | /** @psalm-suppress ImpureFunctionCall */ 103 | return Sequence::defer((static function() use ($captured) { 104 | /** @var V $value */ 105 | foreach (self::detonate($captured)->toSequence()->toList() as $value) { 106 | yield $value; 107 | } 108 | })()); 109 | } 110 | 111 | #[\Override] 112 | public function eitherWay(callable $just, callable $nothing): Maybe 113 | { 114 | $captured = $this->capture(); 115 | 116 | return Maybe::defer(static fn() => self::detonate($captured)->eitherWay($just, $nothing)); 117 | } 118 | 119 | /** 120 | * @return Maybe 121 | */ 122 | private function unwrap(): Maybe 123 | { 124 | /** 125 | * @psalm-suppress InaccessibleProperty 126 | * @psalm-suppress ImpureFunctionCall 127 | */ 128 | return $this->value ??= ($this->deferred)()->memoize(); 129 | } 130 | 131 | /** 132 | * @return array{\WeakReference>, callable(): Maybe} 133 | */ 134 | private function capture(): array 135 | { 136 | /** @psalm-suppress ImpureMethodCall */ 137 | return [ 138 | \WeakReference::create($this), 139 | $this->deferred, 140 | ]; 141 | } 142 | 143 | /** 144 | * @template T 145 | * 146 | * @param array{\WeakReference>, callable(): Maybe} $captured 147 | * 148 | * @return Maybe 149 | */ 150 | private static function detonate(array $captured): Maybe 151 | { 152 | [$ref, $deferred] = $captured; 153 | $self = $ref->get(); 154 | 155 | if (\is_null($self)) { 156 | return $deferred(); 157 | } 158 | 159 | return $self->unwrap(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Either/Defer.php: -------------------------------------------------------------------------------- 1 | 16 | * @psalm-immutable 17 | * @internal 18 | */ 19 | final class Defer implements Implementation 20 | { 21 | /** @var callable(): Either */ 22 | private $deferred; 23 | /** @var ?Either */ 24 | private ?Either $value = null; 25 | 26 | /** 27 | * @param callable(): Either $deferred 28 | */ 29 | public function __construct($deferred) 30 | { 31 | $this->deferred = $deferred; 32 | } 33 | 34 | #[\Override] 35 | public function map(callable $map): self 36 | { 37 | $captured = $this->capture(); 38 | 39 | return new self(static fn() => self::detonate($captured)->map($map)); 40 | } 41 | 42 | #[\Override] 43 | public function flatMap(callable $map): Either 44 | { 45 | $captured = $this->capture(); 46 | 47 | return Either::defer(static fn() => self::detonate($captured)->flatMap($map)); 48 | } 49 | 50 | #[\Override] 51 | public function leftMap(callable $map): self 52 | { 53 | $captured = $this->capture(); 54 | 55 | return new self(static fn() => self::detonate($captured)->leftMap($map)); 56 | } 57 | 58 | #[\Override] 59 | public function match(callable $right, callable $left) 60 | { 61 | return $this->unwrap()->match($right, $left); 62 | } 63 | 64 | #[\Override] 65 | public function otherwise(callable $otherwise): Either 66 | { 67 | $captured = $this->capture(); 68 | 69 | return Either::defer(static fn() => self::detonate($captured)->otherwise($otherwise)); 70 | } 71 | 72 | #[\Override] 73 | public function filter(callable $predicate, callable $otherwise): Implementation 74 | { 75 | $captured = $this->capture(); 76 | 77 | return new self(static fn() => self::detonate($captured)->filter($predicate, $otherwise)); 78 | } 79 | 80 | #[\Override] 81 | public function maybe(): Maybe 82 | { 83 | $captured = $this->capture(); 84 | 85 | return Maybe::defer(static fn() => self::detonate($captured)->maybe()); 86 | } 87 | 88 | #[\Override] 89 | public function attempt(callable $error): Attempt 90 | { 91 | $captured = $this->capture(); 92 | 93 | return Attempt::defer(static fn() => self::detonate($captured)->attempt($error)); 94 | } 95 | 96 | /** 97 | * @return Either 98 | */ 99 | #[\Override] 100 | public function memoize(): Either 101 | { 102 | return $this->unwrap(); 103 | } 104 | 105 | #[\Override] 106 | public function flip(): self 107 | { 108 | $captured = $this->capture(); 109 | 110 | return new self(static fn() => self::detonate($captured)->flip()); 111 | } 112 | 113 | #[\Override] 114 | public function eitherWay(callable $right, callable $left): Either 115 | { 116 | $captured = $this->capture(); 117 | 118 | return Either::defer(static fn() => self::detonate($captured)->eitherWay($right, $left)); 119 | } 120 | 121 | /** 122 | * @return Either 123 | */ 124 | private function unwrap(): Either 125 | { 126 | /** 127 | * @psalm-suppress InaccessibleProperty 128 | * @psalm-suppress ImpureFunctionCall 129 | */ 130 | return $this->value ??= ($this->deferred)()->memoize(); 131 | } 132 | 133 | /** 134 | * @return array{\WeakReference>, callable(): Either} 135 | */ 136 | private function capture(): array 137 | { 138 | /** @psalm-suppress ImpureMethodCall */ 139 | return [ 140 | \WeakReference::create($this), 141 | $this->deferred, 142 | ]; 143 | } 144 | 145 | /** 146 | * @template A 147 | * @template B 148 | * 149 | * @param array{\WeakReference>, callable(): Either} $captured 150 | * 151 | * @return Either 152 | */ 153 | private static function detonate(array $captured): Either 154 | { 155 | [$ref, $deferred] = $captured; 156 | $self = $ref->get(); 157 | 158 | if (\is_null($self)) { 159 | return $deferred(); 160 | } 161 | 162 | return $self->unwrap(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Accumulate.php: -------------------------------------------------------------------------------- 1 | 12 | * @internal Do not use this in your code 13 | * @psalm-immutable Not really immutable but to simplify declaring immutability of other structures 14 | */ 15 | final class Accumulate implements \Iterator 16 | { 17 | /** @var \Generator */ 18 | private \Generator $generator; 19 | /** @var list */ 20 | private array $values = []; 21 | /** @var list> */ 22 | private array $cursors = []; 23 | private bool $started = false; 24 | 25 | /** 26 | * @param \Generator $generator 27 | */ 28 | public function __construct(\Generator $generator) 29 | { 30 | $this->generator = $generator; 31 | } 32 | 33 | /** 34 | * @return S 35 | */ 36 | #[\Override] 37 | public function current(): mixed 38 | { 39 | /** @psalm-suppress InaccessibleProperty */ 40 | $this->started = true; 41 | /** @psalm-suppress UnusedMethodCall */ 42 | $this->pop(); 43 | 44 | /** @var S */ 45 | return \current($this->values); 46 | } 47 | 48 | /** 49 | * @return int<0, max>|null 50 | */ 51 | #[\Override] 52 | public function key(): ?int 53 | { 54 | /** @psalm-suppress InaccessibleProperty */ 55 | $this->started = true; 56 | /** @psalm-suppress UnusedMethodCall */ 57 | $this->pop(); 58 | 59 | return \key($this->values); 60 | } 61 | 62 | #[\Override] 63 | public function next(): void 64 | { 65 | /** @psalm-suppress InaccessibleProperty */ 66 | $this->started = true; 67 | /** @psalm-suppress InaccessibleProperty */ 68 | \next($this->values); 69 | 70 | if ($this->reachedCacheEnd()) { 71 | /** @psalm-suppress ImpureMethodCall */ 72 | $this->generator->next(); 73 | } 74 | } 75 | 76 | #[\Override] 77 | public function rewind(): void 78 | { 79 | if ($this->started && !\is_null($key = $this->key())) { 80 | /** @psalm-suppress InaccessibleProperty */ 81 | $this->cursors[] = $key; 82 | } 83 | 84 | /** @psalm-suppress InaccessibleProperty */ 85 | $this->started = true; 86 | /** @psalm-suppress InaccessibleProperty */ 87 | \reset($this->values); 88 | } 89 | 90 | #[\Override] 91 | public function valid(): bool 92 | { 93 | /** @psalm-suppress InaccessibleProperty */ 94 | $this->started = true; 95 | /** @psalm-suppress ImpureMethodCall */ 96 | $valid = !$this->reachedCacheEnd() || $this->generator->valid(); 97 | 98 | if (!$valid) { 99 | // once the "true" end has been reached we automatically rewind this 100 | // iterator so it is always in a clean state 101 | $this->cleanup(); 102 | } 103 | 104 | return $valid; 105 | } 106 | 107 | public function started(): bool 108 | { 109 | return $this->started; 110 | } 111 | 112 | public function cleanup(): void 113 | { 114 | /** @psalm-suppress InaccessibleProperty */ 115 | $previousCursor = \array_pop($this->cursors); 116 | 117 | if (\is_null($previousCursor)) { 118 | return; 119 | } 120 | 121 | // Re-position the cursor to the previous position before entering a new 122 | // loop. It only iterate over the cached values because the previous 123 | // cursor must be in the cache. 124 | /** @psalm-suppress InaccessibleProperty */ 125 | \reset($this->values); 126 | 127 | while (\is_int(\key($this->values)) && \key($this->values) !== $previousCursor) { 128 | /** @psalm-suppress InaccessibleProperty */ 129 | \next($this->values); 130 | } 131 | } 132 | 133 | private function reachedCacheEnd(): bool 134 | { 135 | return \key($this->values) === null; 136 | } 137 | 138 | private function pop(): void 139 | { 140 | /** @psalm-suppress ImpureMethodCall */ 141 | if ($this->reachedCacheEnd() && $this->generator->valid()) { 142 | /** 143 | * @psalm-suppress InaccessibleProperty 144 | * @psalm-suppress ImpureMethodCall 145 | */ 146 | $this->values[] = $this->generator->current(); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Attempt/Defer.php: -------------------------------------------------------------------------------- 1 | 15 | * @psalm-immutable 16 | * @internal 17 | */ 18 | final class Defer implements Implementation 19 | { 20 | /** @var callable(): Attempt */ 21 | private $deferred; 22 | /** @var ?Attempt */ 23 | private ?Attempt $value = null; 24 | 25 | /** 26 | * @param callable(): Attempt $deferred 27 | */ 28 | public function __construct(callable $deferred) 29 | { 30 | $this->deferred = $deferred; 31 | } 32 | 33 | #[\Override] 34 | public function map(callable $map): self 35 | { 36 | $captured = $this->capture(); 37 | 38 | return new self(static fn() => self::detonate($captured)->map($map)); 39 | } 40 | 41 | #[\Override] 42 | public function flatMap( 43 | callable $map, 44 | callable $exfiltrate, 45 | ): self { 46 | $captured = $this->capture(); 47 | 48 | return new self(static fn() => self::detonate($captured)->flatMap($map)); 49 | } 50 | 51 | #[\Override] 52 | public function guard( 53 | callable $map, 54 | callable $exfiltrate, 55 | ): self { 56 | $captured = $this->capture(); 57 | 58 | return new self(static fn() => self::detonate($captured)->guard($map)); 59 | } 60 | 61 | #[\Override] 62 | public function guardError(): self 63 | { 64 | return $this; 65 | } 66 | 67 | #[\Override] 68 | public function match(callable $result, callable $error) 69 | { 70 | return $this->unwrap()->match($result, $error); 71 | } 72 | 73 | #[\Override] 74 | public function mapError(callable $map): self 75 | { 76 | $captured = $this->capture(); 77 | 78 | return new self(static fn() => self::detonate($captured)->mapError($map)); 79 | } 80 | 81 | #[\Override] 82 | public function recover( 83 | callable $recover, 84 | callable $exfiltrate, 85 | ): self { 86 | $captured = $this->capture(); 87 | 88 | return new self(static fn() => self::detonate($captured)->recover($recover)); 89 | } 90 | 91 | #[\Override] 92 | public function xrecover( 93 | callable $recover, 94 | callable $exfiltrate, 95 | ): self { 96 | $captured = $this->capture(); 97 | 98 | return new self(static fn() => self::detonate($captured)->xrecover($recover)); 99 | } 100 | 101 | #[\Override] 102 | public function maybe(): Maybe 103 | { 104 | $captured = $this->capture(); 105 | 106 | return Maybe::defer(static fn() => self::detonate($captured)->maybe()); 107 | } 108 | 109 | #[\Override] 110 | public function either(): Either 111 | { 112 | $captured = $this->capture(); 113 | 114 | return Either::defer(static fn() => self::detonate($captured)->either()); 115 | } 116 | 117 | #[\Override] 118 | public function memoize(callable $exfiltrate): Implementation 119 | { 120 | return $exfiltrate($this->unwrap()); 121 | } 122 | 123 | #[\Override] 124 | public function eitherWay( 125 | callable $result, 126 | callable $error, 127 | callable $exfiltrate, 128 | ): self { 129 | $captured = $this->capture(); 130 | 131 | return new self( 132 | static fn() => self::detonate($captured)->eitherWay($result, $error), 133 | ); 134 | } 135 | 136 | /** 137 | * @return Attempt 138 | */ 139 | private function unwrap(): Attempt 140 | { 141 | /** 142 | * @psalm-suppress InaccessibleProperty 143 | * @psalm-suppress ImpureFunctionCall 144 | */ 145 | return $this->value ??= ($this->deferred)()->memoize(); 146 | } 147 | 148 | /** 149 | * @return array{\WeakReference>, callable(): Attempt} 150 | */ 151 | private function capture(): array 152 | { 153 | /** @psalm-suppress ImpureMethodCall */ 154 | return [ 155 | \WeakReference::create($this), 156 | $this->deferred, 157 | ]; 158 | } 159 | 160 | /** 161 | * @template A 162 | * 163 | * @param array{\WeakReference>, callable(): Attempt} $captured 164 | * 165 | * @return Attempt 166 | */ 167 | private static function detonate(array $captured): Attempt 168 | { 169 | [$ref, $deferred] = $captured; 170 | $self = $ref->get(); 171 | 172 | if (\is_null($self)) { 173 | return $deferred(); 174 | } 175 | 176 | return $self->unwrap(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Validation.php: -------------------------------------------------------------------------------- 1 | $implementation 15 | */ 16 | private function __construct( 17 | private Validation\Implementation $implementation, 18 | ) { 19 | } 20 | 21 | /** 22 | * @template A 23 | * @template B 24 | * @psalm-pure 25 | * 26 | * @param A $value 27 | * 28 | * @return self 29 | */ 30 | #[\NoDiscard] 31 | public static function success($value): self 32 | { 33 | return new self(Validation\Success::of($value)); 34 | } 35 | 36 | /** 37 | * @template A 38 | * @template B 39 | * @psalm-pure 40 | * 41 | * @param A $value 42 | * 43 | * @return self 44 | */ 45 | #[\NoDiscard] 46 | public static function fail($value): self 47 | { 48 | return new self(Validation\Fail::of($value)); 49 | } 50 | 51 | /** 52 | * @template T 53 | * 54 | * @param callable(S): T $map 55 | * 56 | * @return self 57 | */ 58 | #[\NoDiscard] 59 | public function map(callable $map): self 60 | { 61 | return new self($this->implementation->map($map)); 62 | } 63 | 64 | /** 65 | * @template T 66 | * @template V 67 | * 68 | * @param callable(S): self $map 69 | * 70 | * @return self 71 | */ 72 | #[\NoDiscard] 73 | public function flatMap(callable $map): self 74 | { 75 | return new self($this->implementation->flatMap( 76 | $map, 77 | static fn(self $self) => $self->implementation, 78 | )); 79 | } 80 | 81 | /** 82 | * @template T 83 | * @template V 84 | * 85 | * @param callable(S): self $map 86 | * 87 | * @return self 88 | */ 89 | #[\NoDiscard] 90 | public function guard(callable $map): self 91 | { 92 | return new self($this->implementation->guard( 93 | $map, 94 | static fn(self $self) => $self->implementation, 95 | )); 96 | } 97 | 98 | /** 99 | * @template T 100 | * 101 | * @param callable(F): T $map 102 | * 103 | * @return self 104 | */ 105 | #[\NoDiscard] 106 | public function mapFailures(callable $map): self 107 | { 108 | return new self($this->implementation->mapFailures($map)); 109 | } 110 | 111 | /** 112 | * @template T 113 | * @template V 114 | * 115 | * @param callable(Sequence): self $map 116 | * 117 | * @return self 118 | */ 119 | #[\NoDiscard] 120 | public function otherwise(callable $map): self 121 | { 122 | return $this->implementation->otherwise($map); 123 | } 124 | 125 | /** 126 | * This prevents guarded failures from being recovered. 127 | * 128 | * @template T 129 | * @template V 130 | * 131 | * @param callable(Sequence): self $map 132 | * 133 | * @return self 134 | */ 135 | #[\NoDiscard] 136 | public function xotherwise(callable $map): self 137 | { 138 | return $this->implementation->xotherwise( 139 | $map, 140 | static fn(Validation\Implementation $implementation) => new self($implementation), 141 | ); 142 | } 143 | 144 | /** 145 | * @template A 146 | * @template T 147 | * 148 | * @param self $other 149 | * @param callable(S, A): T $fold 150 | * 151 | * @return self 152 | */ 153 | #[\NoDiscard] 154 | public function and(self $other, callable $fold): self 155 | { 156 | return new self($this->implementation->and( 157 | $other->implementation, 158 | $fold, 159 | )); 160 | } 161 | 162 | /** 163 | * @template T 164 | * 165 | * @param callable(S): T $success 166 | * @param callable(Sequence): T $failure 167 | * 168 | * @return T 169 | */ 170 | #[\NoDiscard] 171 | public function match(callable $success, callable $failure) 172 | { 173 | return $this->implementation->match($success, $failure); 174 | } 175 | 176 | /** 177 | * @return Maybe 178 | */ 179 | #[\NoDiscard] 180 | public function maybe(): Maybe 181 | { 182 | return $this->implementation->maybe(); 183 | } 184 | 185 | /** 186 | * @return Either, S> 187 | */ 188 | #[\NoDiscard] 189 | public function either(): Either 190 | { 191 | return $this->implementation->either(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Immutable 2 | 3 | [![Build Status](https://github.com/Innmind/Immutable/workflows/CI/badge.svg?branch=master)](https://github.com/Innmind/Immutable/actions?query=workflow%3ACI) 4 | [![codecov](https://codecov.io/gh/Innmind/Immutable/branch/develop/graph/badge.svg)](https://codecov.io/gh/Innmind/Immutable) 5 | [![Type Coverage](https://shepherd.dev/github/Innmind/Immutable/coverage.svg)](https://shepherd.dev/github/Innmind/Immutable) 6 | 7 | A set of classes to wrap PHP primitives to build immutable data. 8 | 9 | [Documentation](https://innmind.github.io/Immutable/) 10 | 11 | ## Installation 12 | 13 | ```sh 14 | composer require innmind/immutable 15 | ``` 16 | 17 | ## Usage 18 | 19 | Here are some examples of what you can do: 20 | 21 | ### Sequence 22 | 23 | To be used to wrap an ordered list of elements (elements can be of mixed types). 24 | 25 | ```php 26 | use Innmind\Immutable\Sequence; 27 | 28 | $seq = Sequence::of(24, 42, 'Hitchhiker', 'Magrathea'); 29 | $seq->get(2); // Maybe::just(Hitchhiker) 30 | $another = $seq->drop(2); 31 | $another->toList(); // [Hitchhiker, Magrathea] 32 | $seq->toList(); // [24, 42, Hitchhiker, Magrathea] 33 | 34 | //---- 35 | // this example demonstrates the lazyness capability of the sequence 36 | // precisely here it's able to read a file line by line and echo the lines 37 | // that are less than 42 characters long (without requiring to load the whole 38 | // file in memory) 39 | $someFile = fopen('some/file.txt', 'r'); 40 | $lines = Sequence::lazy(fn() => yield fgets($someFile)) 41 | ->filter(fn($line) => strlen($line) < 42); 42 | // at this point no reading to the file has been done because all methods 43 | // returning a new instance of a sequence will pipeline the operations to do, 44 | // allowing to chain complex logic while accessing the original data once and 45 | // without the need to keep the discarded data along the pipeline in memory 46 | $lines->foreach(fn($line) => echo($line)); 47 | ``` 48 | 49 | For a complete list of methods check [`Sequence`](src/Sequence.php). 50 | 51 | ### Set 52 | 53 | To be used as a collection of unordered elements (elements must be of the same type). 54 | 55 | ```php 56 | use Innmind\Immutable\Set; 57 | 58 | $set = Set::of(24, 42); 59 | $set->equals(Set::of(24, 42)); // true 60 | $set->add(42.0); // psalm will raise an error 61 | ``` 62 | 63 | For a complete list of methods check [`Set`](src/Set.php). 64 | 65 | ### Map 66 | 67 | To be used as a collection of key/value pairs (both keys and values must be of the same type). 68 | 69 | ```php 70 | use Innmind\Immutable\Map; 71 | 72 | $map = Map::of( 73 | [new \stdClass, 42] 74 | [$key = new \stdClass, 24] 75 | ); 76 | $map->size(); // 2, because it's 2 different instances 77 | $map->values()->toList(); // [42, 24] 78 | $map = $map->put($key, 66); 79 | $map->size(); // 2 80 | $map->values()->toList(); // [42, 66] 81 | ``` 82 | 83 | For a complete list of methods check [`Map`](src/Map.php). 84 | 85 | ### Strings 86 | 87 | ```php 88 | use Innmind\Immutable\Str; 89 | 90 | $var = Str::of('the hitchhiker\'s guide to the galaxy'); 91 | echo $var 92 | ->replace('galaxy', '42') // the hitchhiker's guide to the 42 93 | ->drop(18) // guide to the 42 94 | ->toUpper() 95 | ->toString(); // outputs: GUIDE TO THE 42 96 | echo $var->toString(); // outputs: the hitchhiker\'s guide to the galaxy 97 | ``` 98 | 99 | ## Regular expressions 100 | 101 | ```php 102 | use Innmind\Immutable\{ 103 | RegExp, 104 | Str, 105 | }; 106 | 107 | $regexp = RegExp::of('/(?\d+)/'); 108 | $regexp->matches(Str::of('foo123bar')); // true 109 | $regexp->matches(Str::of('foobar')); // false 110 | $regexp->capture(Str::of('foo123bar')); // Map with index `i` set to Str::of('123') 111 | ``` 112 | 113 | ### [BlackBox](https://github.com/innmind/blackbox/) 114 | 115 | This library provides 2 `Set`s that can be used with [`innmind/black-box`](https://packagist.org/packages/innmind/black-box). 116 | 117 | You can use them as follow: 118 | 119 | ```php 120 | use Innmind\BlackBox\{ 121 | PHPUnit\BlackBox, 122 | Set, 123 | }; 124 | use Fixtures\Innmind\Immutable; 125 | 126 | class SomeTest extends \PHPUnit\Framework\TestCase 127 | { 128 | use BlackBox; 129 | 130 | public function testSomeProperty() 131 | { 132 | $this 133 | ->forAll( 134 | Immutable\Set::of( 135 | Set\RealNumbers::any(), 136 | ), 137 | Immutable\Sequence::of( 138 | Set\Uuid::any(), 139 | ), 140 | ) 141 | ->then(function($set, $sequence) { 142 | // $set is an instance of \Innmind\Immutable\Set 143 | // $sequence is an instance of \Innmind\Immutable\Sequence 144 | 145 | // write your test here 146 | }); 147 | } 148 | } 149 | ``` 150 | -------------------------------------------------------------------------------- /docs/structures/fold.md: -------------------------------------------------------------------------------- 1 | # `Fold` 2 | 3 | ??? warning "Deprecated" 4 | `Fold` is deprecated and will be removed in the next major release. 5 | 6 | The `Fold` monad is intented to work with _(infinite) stream of data_ by folding each element to a single value. This monad distinguishes between the type used to fold and the result type, this allows to inform the _stream_ that it's no longer necessary to extract elements as the folding is done. 7 | 8 | An example is reading from a socket as it's an infinite stream of strings: 9 | 10 | ```php 11 | $socket = \stream_socket_client(/* args */); 12 | /** @var Fold, list> */ 13 | $fold = Fold::with([]); 14 | 15 | do { 16 | // production code should wait for the socket to be "ready" 17 | $line = \fgets($socket); 18 | 19 | if ($line === false) { 20 | $fold = Fold::fail('socket not readable'); 21 | } 22 | 23 | $fold = $fold 24 | ->map(static fn($lines) => \array_merge($lines, [$line])) 25 | ->flatMap(static fn($lines) => match (\end($lines)) { 26 | "quit\n" => Fold::result($lines), 27 | default => Fold::with($lines), 28 | }); 29 | $continue = $fold->match( 30 | static fn() => true, // still folding 31 | static fn() => false, // got a result so stop 32 | static fn() => false, // got a failure so stop 33 | ); 34 | } while ($continue); 35 | 36 | $fold->match( 37 | static fn() => null, // unreachable in this case because no more folding outside the loop 38 | static fn($lines) => \var_dump($lines), 39 | static fn($failure) => throw new \RuntimeException($failure), 40 | ); 41 | ``` 42 | 43 | This example will read all lines from the socket until one line contains `quit\n` then the loop will stop and either dump all the lines to the output or `throw new RuntimeException('socket not reachable')`. 44 | 45 | ## `::with()` 46 | 47 | This named constructor accepts a value with the notion that more elements are necessary to compute a result 48 | 49 | ## `::result()` 50 | 51 | This named constructor accepts a _result_ value meaning that folding is finished. 52 | 53 | ## `::fail()` 54 | 55 | This named constructor accepts a _failure_ value meaning that the folding operation failed and no _result_ will be reachable. 56 | 57 | ## `->map()` 58 | 59 | This method allows to transform the value being folded. 60 | 61 | ```php 62 | $fold = Fold::with([])->map(static fn(array $folding) => new \ArrayObject($folding)); 63 | ``` 64 | 65 | ## `->flatMap()` 66 | 67 | This method allows to both change the value and the _state_, for example switching from _folding_ to _result_. 68 | 69 | ```php 70 | $someElement = /* some data */; 71 | $fold = Fold::with([])->flatMap(static fn($elements) => match ($someElement) { 72 | 'finish' => Fold::result($elements), 73 | default => Fold::with(\array_merge($elements, [$someElement])), 74 | }); 75 | ``` 76 | 77 | ## `->mapResult()` 78 | 79 | Same as [`->map()`](#-map) except that it will transform the _result_ value when there is one. 80 | 81 | ## `->mapFailure()` 82 | 83 | Same as [`->map()`](#-map) except that it will transform the _failure_ value when there is one. 84 | 85 | ## `->maybe()` 86 | 87 | This will return the _terminal_ value of the folding, meaning either a _result_ or a _failure_. 88 | 89 | ```php 90 | Fold::with([])->maybe()->match( 91 | static fn() => null, // not called as still folding 92 | static fn() => doStuff(), // called as it is still folding 93 | ); 94 | Fold::result([])->maybe()->match( 95 | static fn($either) => $either->match( 96 | static fn($result) => $result, // the value here is the array passed to ::result() above 97 | static fn() => null, // not called as it doesn't contain a failure 98 | ), 99 | static fn() => null, // not called as we have a result 100 | ); 101 | Fold::fail('some error')->maybe()->match( 102 | static fn($either) => $either->match( 103 | static fn() => null, // not called as we have a failure 104 | static fn($error) => var_dump($error), // the value here is the string passed to ::fail() above 105 | ), 106 | static fn() => null, // not called as we have a result 107 | ); 108 | ``` 109 | 110 | ## `->match()` 111 | 112 | This method allows to extract the value contained in the object. 113 | 114 | ```php 115 | Fold::with([])->match( 116 | static fn($folding) => doStuf($folding), // value from ::with() 117 | static fn() => null, // not called 118 | static fn() => null, // not called 119 | ); 120 | Fold::result([])->match( 121 | static fn() => null, // not called 122 | static fn($result) => doStuf($result), // value from ::result() 123 | static fn() => null, // not called 124 | ); 125 | Fold::fail('some error')->match( 126 | static fn() => null, // not called 127 | static fn() => null, // not called 128 | static fn($error) => doStuf($error), // value from ::fail() 129 | ); 130 | ``` 131 | -------------------------------------------------------------------------------- /src/Sequence/Sink.php: -------------------------------------------------------------------------------- 1 | $implementation 21 | * @param C $carry 22 | */ 23 | private function __construct( 24 | private Implementation $implementation, 25 | private mixed $carry, 26 | ) { 27 | } 28 | 29 | /** 30 | * @internal 31 | * @psalm-pure 32 | * @template A 33 | * @template B 34 | * 35 | * @param Implementation $implementation 36 | * @param B $carry 37 | * 38 | * @return self 39 | */ 40 | public static function of(Implementation $implementation, mixed $carry): self 41 | { 42 | return new self($implementation, $carry); 43 | } 44 | 45 | /** 46 | * @param callable(C, T, Sink\Continuation): Sink\Continuation $reducer 47 | * 48 | * @return C 49 | */ 50 | #[\NoDiscard] 51 | public function until(callable $reducer): mixed 52 | { 53 | return $this->implementation->sink( 54 | $this->carry, 55 | $reducer, 56 | ); 57 | } 58 | 59 | /** 60 | * This will consume all the values from the Sequence as long as a value is 61 | * contained in the returned Maybe. 62 | * 63 | * @param callable(C, T): Maybe $reducer 64 | * 65 | * @return Maybe 66 | */ 67 | #[\NoDiscard] 68 | public function maybe(callable $reducer): Maybe 69 | { 70 | return $this->implementation->sink( 71 | Maybe::just($this->carry), 72 | static function($carry, $value, $continuation) use ($reducer) { 73 | /** 74 | * @var Maybe $carry 75 | * @var T $value 76 | */ 77 | 78 | /** @psalm-suppress MixedArgument */ 79 | $maybe = $carry 80 | ->flatMap(static fn($carry) => $reducer($carry, $value)) 81 | ->memoize(); 82 | 83 | return $maybe->match( 84 | static fn() => $continuation->continue($maybe), 85 | static fn() => $continuation->stop($maybe), 86 | ); 87 | }, 88 | ); 89 | } 90 | 91 | /** 92 | * This will consume all the values from the Sequence as long as a right 93 | * value is contained in the returned Either. 94 | * 95 | * @template E 96 | * 97 | * @param callable(C, T): Either $reducer 98 | * 99 | * @return Either 100 | */ 101 | #[\NoDiscard] 102 | public function either(callable $reducer): Either 103 | { 104 | /** @var Either */ 105 | $carry = Either::right($this->carry); 106 | 107 | return $this->implementation->sink( 108 | $carry, 109 | static function($carry, $value, $continuation) use ($reducer) { 110 | /** 111 | * @var Either $carry 112 | * @var T $value 113 | */ 114 | 115 | /** @psalm-suppress MixedArgument */ 116 | $either = $carry 117 | ->flatMap(static fn($carry) => $reducer($carry, $value)) 118 | ->memoize(); 119 | 120 | return $either->match( 121 | static fn() => $continuation->continue($either), 122 | static fn() => $continuation->stop($either), 123 | ); 124 | }, 125 | ); 126 | } 127 | 128 | /** 129 | * This will consume all the values from the Sequence as long as a value is 130 | * contained in the returned Attempt. 131 | * 132 | * @param callable(C, T): Attempt $reducer 133 | * 134 | * @return Attempt 135 | */ 136 | #[\NoDiscard] 137 | public function attempt(callable $reducer): Attempt 138 | { 139 | return $this->implementation->sink( 140 | Attempt::result($this->carry), 141 | static function($carry, $value, $continuation) use ($reducer) { 142 | /** 143 | * @var Attempt $carry 144 | * @var T $value 145 | */ 146 | 147 | /** @psalm-suppress MixedArgument */ 148 | $attempt = $carry 149 | ->flatMap(static fn($carry) => $reducer($carry, $value)) 150 | ->memoize(); 151 | 152 | return $attempt->match( 153 | static fn() => $continuation->continue($attempt), 154 | static fn() => $continuation->stop($attempt), 155 | ); 156 | }, 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Sequence/Aggregate.php: -------------------------------------------------------------------------------- 1 | $values 17 | */ 18 | public function __construct( 19 | private \Iterator $values, 20 | ) { 21 | } 22 | 23 | /** 24 | * @template A 25 | * 26 | * @param callable(T|A, T): \Iterator $map 27 | * 28 | * @return \Generator 29 | */ 30 | public function __invoke(callable $map): \Generator 31 | { 32 | // we use an object to check if the aggregate below as any value in order 33 | // to be sure there is no false equality (as the values may contain null) 34 | $void = new \stdClass; 35 | 36 | /** @psalm-suppress ImpureMethodCall */ 37 | $this->values->rewind(); 38 | 39 | /** @psalm-suppress ImpureMethodCall */ 40 | if (!$this->values->valid()) { 41 | return; 42 | } 43 | 44 | /** @psalm-suppress ImpureMethodCall */ 45 | $n2 = $this->values->current(); 46 | /** @psalm-suppress ImpureMethodCall */ 47 | $this->values->next(); 48 | 49 | /** @psalm-suppress ImpureMethodCall */ 50 | if (!$this->values->valid()) { 51 | yield $n2; 52 | 53 | return; 54 | } 55 | 56 | /** @psalm-suppress ImpureMethodCall */ 57 | $n1 = $this->values->current(); 58 | 59 | /** @psalm-suppress ImpureMethodCall */ 60 | while ($this->values->valid()) { 61 | /** 62 | * @psalm-suppress PossiblyNullArgument 63 | * @psalm-suppress ImpureFunctionCall 64 | */ 65 | $aggregate = $this->walk($map($n2, $n1), $void); 66 | 67 | foreach ($aggregate as $element) { 68 | yield $element; 69 | } 70 | 71 | /** 72 | * @psalm-suppress ImpureMethodCall 73 | * @var T|A 74 | */ 75 | $n2 = $aggregate->getReturn(); 76 | 77 | if ($n2 === $void) { 78 | // enforce returning at least one element to prevent confusing 79 | // behavior 80 | // the alternative would be to pull 2 elements from the source 81 | // values but if $map always return an empty sequence then the 82 | // whole sequence will only contain the last source value which 83 | // can be confusing 84 | throw new LogicException('Aggregates must always return at least one element'); 85 | } 86 | 87 | /** @psalm-suppress ImpureMethodCall */ 88 | $this->values->next(); 89 | 90 | // this condition is to accomodate the Accumulate iterator that will 91 | // always create a new element when calling current 92 | /** @psalm-suppress ImpureMethodCall */ 93 | if (!$this->values->valid()) { 94 | break; 95 | } 96 | 97 | /** @psalm-suppress ImpureMethodCall */ 98 | $n1 = $this->values->current(); 99 | } 100 | 101 | yield $n2; 102 | } 103 | 104 | /** 105 | * @template W 106 | * @param \Iterator $values 107 | * 108 | * @return \Generator 109 | */ 110 | private function walk(\Iterator $values, \stdClass $void): \Generator 111 | { 112 | /** @psalm-suppress ImpureMethodCall */ 113 | $values->rewind(); 114 | 115 | /** @psalm-suppress ImpureMethodCall */ 116 | if (!$values->valid()) { 117 | return $void; 118 | } 119 | 120 | /** @psalm-suppress ImpureMethodCall */ 121 | $n2 = $values->current(); 122 | /** @psalm-suppress ImpureMethodCall */ 123 | $values->next(); 124 | 125 | /** @psalm-suppress ImpureMethodCall */ 126 | if (!$values->valid()) { 127 | return $n2; 128 | } 129 | 130 | /** @psalm-suppress ImpureMethodCall */ 131 | $n1 = $values->current(); 132 | 133 | /** @psalm-suppress ImpureMethodCall */ 134 | while ($values->valid()) { 135 | yield $n2; 136 | /** @psalm-suppress ImpureMethodCall */ 137 | $values->next(); 138 | $n2 = $n1; 139 | 140 | // this condition is to accomodate the Accumulate iterator that will 141 | // always create a new element when calling current 142 | /** @psalm-suppress ImpureMethodCall */ 143 | if (!$values->valid()) { 144 | break; 145 | } 146 | 147 | /** @psalm-suppress ImpureMethodCall */ 148 | $n1 = $values->current(); 149 | } 150 | 151 | return $n2; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Validation/Success.php: -------------------------------------------------------------------------------- 1 | 17 | * @psalm-immutable 18 | */ 19 | final class Success implements Implementation 20 | { 21 | /** 22 | * @param S $value 23 | */ 24 | private function __construct( 25 | private mixed $value, 26 | ) { 27 | } 28 | 29 | /** 30 | * @template A 31 | * @template B 32 | * @psalm-pure 33 | * 34 | * @param A $value 35 | * 36 | * @return self 37 | */ 38 | public static function of($value): self 39 | { 40 | return new self($value); 41 | } 42 | 43 | /** 44 | * @template T 45 | * 46 | * @param callable(S): T $map 47 | * 48 | * @return Implementation 49 | */ 50 | #[\Override] 51 | public function map(callable $map): Implementation 52 | { 53 | /** @psalm-suppress ImpureFunctionCall */ 54 | return new self($map($this->value)); 55 | } 56 | 57 | /** 58 | * @template T 59 | * @template V 60 | * 61 | * @param callable(S): Validation $map 62 | * @param pure-callable(Validation): Implementation $exfiltrate 63 | * 64 | * @return Implementation 65 | */ 66 | #[\Override] 67 | public function flatMap(callable $map, callable $exfiltrate): Implementation 68 | { 69 | /** @psalm-suppress ImpureFunctionCall */ 70 | return $exfiltrate($map($this->value)); 71 | } 72 | 73 | /** 74 | * @template T 75 | * @template V 76 | * 77 | * @param callable(S): Validation $map 78 | * @param pure-callable(Validation): Implementation $exfiltrate 79 | * 80 | * @return Implementation 81 | */ 82 | #[\Override] 83 | public function guard(callable $map, callable $exfiltrate): Implementation 84 | { 85 | /** @psalm-suppress ImpureFunctionCall */ 86 | return $exfiltrate($map($this->value))->guardFailures(); 87 | } 88 | 89 | #[\Override] 90 | public function guardFailures(): self 91 | { 92 | return $this; 93 | } 94 | 95 | /** 96 | * @template T 97 | * 98 | * @param callable(F): T $map 99 | * 100 | * @return Implementation 101 | */ 102 | #[\Override] 103 | public function mapFailures(callable $map): Implementation 104 | { 105 | /** @var Implementation */ 106 | return $this; 107 | } 108 | 109 | /** 110 | * @template T 111 | * @template V 112 | * 113 | * @param callable(Sequence): Validation $map 114 | * 115 | * @return Validation 116 | */ 117 | #[\Override] 118 | public function otherwise(callable $map): Validation 119 | { 120 | return Validation::success($this->value); 121 | } 122 | 123 | /** 124 | * @template T 125 | * @template V 126 | * 127 | * @param callable(Sequence): Validation $map 128 | * @param callable(Implementation): Validation $wrap 129 | * 130 | * @return Validation 131 | */ 132 | #[\Override] 133 | public function xotherwise( 134 | callable $map, 135 | callable $wrap, 136 | ): Validation { 137 | return Validation::success($this->value); 138 | } 139 | 140 | /** 141 | * @template A 142 | * @template T 143 | * 144 | * @param Implementation $other 145 | * @param callable(S, A): T $fold 146 | * 147 | * @return Implementation 148 | */ 149 | #[\Override] 150 | public function and(Implementation $other, callable $fold): Implementation 151 | { 152 | if ($other instanceof self) { 153 | /** @psalm-suppress ImpureFunctionCall */ 154 | return new self($fold($this->value, $other->value)); 155 | } 156 | 157 | /** @var Implementation */ 158 | return $other; 159 | } 160 | 161 | /** 162 | * @template T 163 | * 164 | * @param callable(S): T $success 165 | * @param callable(Sequence): T $failure 166 | * 167 | * @return T 168 | */ 169 | #[\Override] 170 | public function match(callable $success, callable $failure) 171 | { 172 | /** @psalm-suppress ImpureFunctionCall */ 173 | return $success($this->value); 174 | } 175 | 176 | /** 177 | * @return Maybe 178 | */ 179 | #[\Override] 180 | public function maybe(): Maybe 181 | { 182 | return Maybe::just($this->value); 183 | } 184 | 185 | /** 186 | * @return Either, S> 187 | */ 188 | #[\Override] 189 | public function either(): Either 190 | { 191 | return Either::right($this->value); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Either.php: -------------------------------------------------------------------------------- 1 | $either 22 | */ 23 | private function __construct( 24 | private Implementation $either, 25 | ) { 26 | } 27 | 28 | /** 29 | * @template A 30 | * @template B 31 | * @psalm-pure 32 | * 33 | * @param A $value 34 | * 35 | * @return self 36 | */ 37 | #[\NoDiscard] 38 | public static function left($value): self 39 | { 40 | return new self(new Left($value)); 41 | } 42 | 43 | /** 44 | * @template A 45 | * @template B 46 | * @psalm-pure 47 | * 48 | * @param B $value 49 | * 50 | * @return self 51 | */ 52 | #[\NoDiscard] 53 | public static function right($value): self 54 | { 55 | return new self(new Right($value)); 56 | } 57 | 58 | /** 59 | * This method is to be used for IO operations 60 | * 61 | * @template A 62 | * @template B 63 | * @psalm-pure 64 | * 65 | * @param callable(): self $deferred 66 | * 67 | * @return self 68 | */ 69 | #[\NoDiscard] 70 | public static function defer(callable $deferred): self 71 | { 72 | return new self(new Defer($deferred)); 73 | } 74 | 75 | /** 76 | * @template T 77 | * 78 | * @param callable(R): T $map 79 | * 80 | * @return self 81 | */ 82 | #[\NoDiscard] 83 | public function map(callable $map): self 84 | { 85 | return new self($this->either->map($map)); 86 | } 87 | 88 | /** 89 | * @template A 90 | * @template B 91 | * 92 | * @param callable(R): Either $map 93 | * 94 | * @return Either 95 | */ 96 | #[\NoDiscard] 97 | public function flatMap(callable $map): self 98 | { 99 | return $this->either->flatMap($map); 100 | } 101 | 102 | /** 103 | * @template T 104 | * 105 | * @param callable(L): T $map 106 | * 107 | * @return self 108 | */ 109 | #[\NoDiscard] 110 | public function leftMap(callable $map): self 111 | { 112 | return new self($this->either->leftMap($map)); 113 | } 114 | 115 | /** 116 | * @template T 117 | * 118 | * @param callable(R): T $right 119 | * @param callable(L): T $left 120 | * 121 | * @return T 122 | */ 123 | #[\NoDiscard] 124 | public function match(callable $right, callable $left) 125 | { 126 | return $this->either->match($right, $left); 127 | } 128 | 129 | /** 130 | * @template A 131 | * @template B 132 | * 133 | * @param callable(L): Either $otherwise 134 | * 135 | * @return Either 136 | */ 137 | #[\NoDiscard] 138 | public function otherwise(callable $otherwise): self 139 | { 140 | return $this->either->otherwise($otherwise); 141 | } 142 | 143 | /** 144 | * @template A 145 | * 146 | * @param callable(R): bool $predicate 147 | * @param callable(): A $otherwise 148 | * 149 | * @return self 150 | */ 151 | #[\NoDiscard] 152 | public function filter(callable $predicate, callable $otherwise): self 153 | { 154 | return new self($this->either->filter($predicate, $otherwise)); 155 | } 156 | 157 | /** 158 | * @return Maybe 159 | */ 160 | #[\NoDiscard] 161 | public function maybe(): Maybe 162 | { 163 | return $this->either->maybe(); 164 | } 165 | 166 | /** 167 | * @param callable(L): \Throwable $error 168 | * 169 | * @return Attempt 170 | */ 171 | #[\NoDiscard] 172 | public function attempt(callable $error): Attempt 173 | { 174 | return $this->either->attempt($error); 175 | } 176 | 177 | /** 178 | * Force loading the value in memory (only useful for a deferred Either) 179 | * 180 | * @return self 181 | */ 182 | #[\NoDiscard] 183 | public function memoize(): self 184 | { 185 | return $this->either->memoize(); 186 | } 187 | 188 | /** 189 | * Switch the sides of the values, left becomes right and right left 190 | * 191 | * @return self 192 | */ 193 | #[\NoDiscard] 194 | public function flip(): self 195 | { 196 | return new self($this->either->flip()); 197 | } 198 | 199 | /** 200 | * @template A 201 | * @template B 202 | * 203 | * @param callable(R): self $right 204 | * @param callable(L): self $left 205 | * 206 | * @return self 207 | */ 208 | #[\NoDiscard] 209 | public function eitherWay(callable $right, callable $left): self 210 | { 211 | return $this->either->eitherWay($right, $left); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /docs/assets/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Monaspace Neon"; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url("../fonts/MonaspaceNeon-Regular.woff"); 6 | } 7 | 8 | :root { 9 | --md-code-font: "Monaspace Neon"; 10 | } 11 | 12 | :root { 13 | --light-md-code-hl-number-color: #f76d47; 14 | --light-md-code-hl-function-color: #6384b9; 15 | --light-md-code-hl-operator-color: #39adb5; 16 | --light-md-code-hl-constant-color: #7c4dff; 17 | --light-md-code-hl-string-color: #9fc06f; 18 | --light-md-code-hl-punctuation-color: #39adb5; 19 | --light-md-code-hl-keyword-color: #7c4dff; 20 | --light-md-code-hl-variable-color: #80cbc4; 21 | --light-md-code-hl-comment-color: #ccd7da; 22 | --light-md-code-bg-color: #fafafa; 23 | --light-md-code-fg-color: #ffb62c; 24 | --light-md-code-hl-variable-color: #6384b9; 25 | --dark-md-code-hl-number-color: #f78c6c; 26 | --dark-md-code-hl-function-color: #82aaff; 27 | --dark-md-code-hl-operator-color: #89ddff; 28 | --dark-md-code-hl-constant-color: #c792ea; 29 | --dark-md-code-hl-string-color: #c3e88d; 30 | --dark-md-code-hl-punctuation-color: #89ddff; 31 | --dark-md-code-hl-keyword-color: #c792ea; 32 | --dark-md-code-hl-variable-color: #e8f9f9; 33 | --dark-md-code-hl-comment-color: #546e7a; 34 | --dark-md-code-bg-color: #263238; 35 | --dark-md-code-fg-color: #ffcb6b; 36 | --dark-md-code-hl-variable-color: #82aaff; 37 | } 38 | 39 | @media (prefers-color-scheme: light) { 40 | .language-php > * { 41 | --md-code-hl-number-color: var(--light-md-code-hl-number-color); 42 | --md-code-hl-function-color: var(--light-md-code-hl-function-color); 43 | --md-code-hl-operator-color: var(--light-md-code-hl-operator-color); 44 | --md-code-hl-constant-color: var(--light-md-code-hl-constant-color); 45 | --md-code-hl-string-color: var(--light-md-code-hl-string-color); 46 | --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color); 47 | --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color); 48 | --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); 49 | --md-code-hl-comment-color: var(--light-md-code-hl-comment-color); 50 | --md-code-bg-color: var(--light-md-code-bg-color); 51 | --md-code-fg-color: var(--light-md-code-fg-color); 52 | } 53 | 54 | .language-php .na { 55 | --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); 56 | } 57 | } 58 | 59 | [data-md-color-media="(prefers-color-scheme: light)"] .language-php > * { 60 | --md-code-hl-number-color: var(--light-md-code-hl-number-color); 61 | --md-code-hl-function-color: var(--light-md-code-hl-function-color); 62 | --md-code-hl-operator-color: var(--light-md-code-hl-operator-color); 63 | --md-code-hl-constant-color: var(--light-md-code-hl-constant-color); 64 | --md-code-hl-string-color: var(--light-md-code-hl-string-color); 65 | --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color); 66 | --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color); 67 | --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); 68 | --md-code-hl-comment-color: var(--light-md-code-hl-comment-color); 69 | --md-code-bg-color: var(--light-md-code-bg-color); 70 | --md-code-fg-color: var(--light-md-code-fg-color); 71 | } 72 | 73 | [data-md-color-media="(prefers-color-scheme: light)"] .language-php .na { 74 | --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); 75 | } 76 | 77 | @media (prefers-color-scheme: dark) { 78 | .language-php > * { 79 | --md-code-hl-number-color: var(--dark-md-code-hl-number-color); 80 | --md-code-hl-function-color: var(--dark-md-code-hl-function-color); 81 | --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color); 82 | --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color); 83 | --md-code-hl-string-color: var(--dark-md-code-hl-string-color); 84 | --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color); 85 | --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color); 86 | --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); 87 | --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color); 88 | --md-code-bg-color: var(--dark-md-code-bg-color); 89 | --md-code-fg-color: var(--dark-md-code-fg-color); 90 | } 91 | 92 | .language-php .na { 93 | --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); 94 | } 95 | } 96 | 97 | [data-md-color-media="(prefers-color-scheme: dark)"] .language-php > * { 98 | --md-code-hl-number-color: var(--dark-md-code-hl-number-color); 99 | --md-code-hl-function-color: var(--dark-md-code-hl-function-color); 100 | --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color); 101 | --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color); 102 | --md-code-hl-string-color: var(--dark-md-code-hl-string-color); 103 | --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color); 104 | --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color); 105 | --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); 106 | --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color); 107 | --md-code-bg-color: var(--dark-md-code-bg-color); 108 | --md-code-fg-color: var(--dark-md-code-fg-color); 109 | } 110 | 111 | [data-md-color-media="(prefers-color-scheme: dark)"] .language-php .na { 112 | --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); 113 | } 114 | -------------------------------------------------------------------------------- /src/Map/Uninitialized.php: -------------------------------------------------------------------------------- 1 | 19 | * @psalm-immutable 20 | */ 21 | final class Uninitialized implements Implementation 22 | { 23 | /** 24 | * @param T $key 25 | * @param S $value 26 | * 27 | * @return Implementation 28 | */ 29 | #[\Override] 30 | public function __invoke($key, $value): Implementation 31 | { 32 | return self::open($key, $value); 33 | } 34 | 35 | /** 36 | * @template A 37 | * @template B 38 | * @psalm-pure 39 | * 40 | * @param A $key 41 | * @param B $value 42 | * 43 | * @return Implementation 44 | */ 45 | public static function open($key, $value): Implementation 46 | { 47 | return ObjectKeys::of($key, $value) 48 | ->otherwise(static fn() => Primitive::of($key, $value)) 49 | ->match( 50 | static fn($implementation) => $implementation, 51 | static fn() => DoubleIndex::of($key, $value), 52 | ); 53 | } 54 | 55 | #[\Override] 56 | public function size(): int 57 | { 58 | return 0; 59 | } 60 | 61 | #[\Override] 62 | public function count(): int 63 | { 64 | return $this->size(); 65 | } 66 | 67 | /** 68 | * @param T $key 69 | * 70 | * @return Maybe 71 | */ 72 | #[\Override] 73 | public function get($key): Maybe 74 | { 75 | return Maybe::nothing(); 76 | } 77 | 78 | /** 79 | * @param T $key 80 | */ 81 | #[\Override] 82 | public function contains($key): bool 83 | { 84 | return false; 85 | } 86 | 87 | /** 88 | * @return self 89 | */ 90 | #[\Override] 91 | public function clear(): self 92 | { 93 | return $this; 94 | } 95 | 96 | /** 97 | * @param Implementation $map 98 | */ 99 | #[\Override] 100 | public function equals(Implementation $map): bool 101 | { 102 | return $map->empty(); 103 | } 104 | 105 | /** 106 | * @param callable(T, S): bool $predicate 107 | * 108 | * @return self 109 | */ 110 | #[\Override] 111 | public function filter(callable $predicate): self 112 | { 113 | return $this; 114 | } 115 | 116 | /** 117 | * @param callable(T, S): void $function 118 | */ 119 | #[\Override] 120 | public function foreach(callable $function): SideEffect 121 | { 122 | return SideEffect::identity(); 123 | } 124 | 125 | /** 126 | * @template D 127 | * 128 | * @param callable(T, S): D $discriminator 129 | * 130 | * @return Map> 131 | */ 132 | #[\Override] 133 | public function groupBy(callable $discriminator): Map 134 | { 135 | /** @var Map> */ 136 | return Map::of(); 137 | } 138 | 139 | /** 140 | * @return Set 141 | */ 142 | #[\Override] 143 | public function keys(): Set 144 | { 145 | /** @var Set */ 146 | return Set::of(); 147 | } 148 | 149 | /** 150 | * @return Sequence 151 | */ 152 | #[\Override] 153 | public function values(): Sequence 154 | { 155 | /** @var Sequence */ 156 | return Sequence::of(); 157 | } 158 | 159 | /** 160 | * @template B 161 | * 162 | * @param callable(T, S): B $function 163 | * 164 | * @return self 165 | */ 166 | #[\Override] 167 | public function map(callable $function): self 168 | { 169 | return new self; 170 | } 171 | 172 | /** 173 | * @param T $key 174 | * 175 | * @return self 176 | */ 177 | #[\Override] 178 | public function remove($key): self 179 | { 180 | return $this; 181 | } 182 | 183 | /** 184 | * @param Implementation $map 185 | * 186 | * @return Implementation 187 | */ 188 | #[\Override] 189 | public function merge(Implementation $map): Implementation 190 | { 191 | return $map; 192 | } 193 | 194 | /** 195 | * @param callable(T, S): bool $predicate 196 | * 197 | * @return Map> 198 | */ 199 | #[\Override] 200 | public function partition(callable $predicate): Map 201 | { 202 | return Map::of( 203 | [true, $this->clearMap()], 204 | [false, $this->clearMap()], 205 | ); 206 | } 207 | 208 | /** 209 | * @template I 210 | * @template R 211 | * 212 | * @param I $carry 213 | * @param callable(I|R, T, S): R $reducer 214 | * 215 | * @return I|R 216 | */ 217 | #[\Override] 218 | public function reduce($carry, callable $reducer) 219 | { 220 | return $carry; 221 | } 222 | 223 | #[\Override] 224 | public function empty(): bool 225 | { 226 | return true; 227 | } 228 | 229 | #[\Override] 230 | public function find(callable $predicate): Maybe 231 | { 232 | /** @var Maybe> */ 233 | return Maybe::nothing(); 234 | } 235 | 236 | #[\Override] 237 | public function toSequence(): Sequence 238 | { 239 | return Sequence::of(); 240 | } 241 | 242 | /** 243 | * @return Map 244 | */ 245 | private function clearMap(): Map 246 | { 247 | return Map::of(); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/Maybe.php: -------------------------------------------------------------------------------- 1 | $maybe 21 | */ 22 | private function __construct( 23 | private Implementation $maybe, 24 | ) { 25 | } 26 | 27 | /** 28 | * @template V 29 | * @psalm-pure 30 | * 31 | * @param V $value 32 | * 33 | * @return self 34 | */ 35 | #[\NoDiscard] 36 | public static function just($value): self 37 | { 38 | return new self(new Just($value)); 39 | } 40 | 41 | /** 42 | * @psalm-pure 43 | */ 44 | #[\NoDiscard] 45 | public static function nothing(): self 46 | { 47 | return new self(new Nothing); 48 | } 49 | 50 | /** 51 | * @template V 52 | * @psalm-pure 53 | * 54 | * @param V|null $value 55 | * 56 | * @return self 57 | */ 58 | #[\NoDiscard] 59 | public static function of($value): self 60 | { 61 | if (\is_null($value)) { 62 | return self::nothing(); 63 | } 64 | 65 | return self::just($value); 66 | } 67 | 68 | /** 69 | * This method is to be used for IO operations 70 | * 71 | * @template V 72 | * @psalm-pure 73 | * 74 | * @param callable(): self $deferred 75 | * 76 | * @return self 77 | */ 78 | #[\NoDiscard] 79 | public static function defer(callable $deferred): self 80 | { 81 | return new self(new Defer($deferred)); 82 | } 83 | 84 | /** 85 | * The comprehension is called only when all values exist 86 | * 87 | * @psalm-pure 88 | * @no-named-arguments 89 | */ 90 | #[\NoDiscard] 91 | public static function all(self $first, self ...$rest): Maybe\Comprehension 92 | { 93 | return Maybe\Comprehension::of($first, ...$rest); 94 | } 95 | 96 | /** 97 | * @template V 98 | * 99 | * @param callable(T): V $map 100 | * 101 | * @return self 102 | */ 103 | #[\NoDiscard] 104 | public function map(callable $map): self 105 | { 106 | return new self($this->maybe->map($map)); 107 | } 108 | 109 | /** 110 | * @template V 111 | * 112 | * @param callable(T): Maybe $map 113 | * 114 | * @return Maybe 115 | */ 116 | #[\NoDiscard] 117 | public function flatMap(callable $map): self 118 | { 119 | return $this->maybe->flatMap($map); 120 | } 121 | 122 | /** 123 | * @template V 124 | * 125 | * @param callable(T): V $just 126 | * @param callable(): V $nothing 127 | * 128 | * @return V 129 | */ 130 | #[\NoDiscard] 131 | public function match(callable $just, callable $nothing) 132 | { 133 | return $this->maybe->match($just, $nothing); 134 | } 135 | 136 | /** 137 | * @template V 138 | * 139 | * @param callable(): Maybe $otherwise 140 | * 141 | * @return Maybe 142 | */ 143 | #[\NoDiscard] 144 | public function otherwise(callable $otherwise): self 145 | { 146 | return $this->maybe->otherwise($otherwise); 147 | } 148 | 149 | /** 150 | * This is the same behaviour as `filter` but it allows Psalm to understand 151 | * the type of the values contained in the returned Maybe 152 | * 153 | * @template S 154 | * 155 | * @param Predicate $predicate 156 | * 157 | * @return self 158 | */ 159 | #[\NoDiscard] 160 | public function keep(Predicate $predicate): self 161 | { 162 | /** @var self */ 163 | return $this->filter($predicate); 164 | } 165 | 166 | /** 167 | * @param callable(T): bool $predicate 168 | * 169 | * @return self 170 | */ 171 | #[\NoDiscard] 172 | public function filter(callable $predicate): self 173 | { 174 | return new self($this->maybe->filter($predicate)); 175 | } 176 | 177 | /** 178 | * @param callable(T): bool $predicate 179 | * 180 | * @return self 181 | */ 182 | #[\NoDiscard] 183 | public function exclude(callable $predicate): self 184 | { 185 | /** @psalm-suppress MixedArgument */ 186 | return $this->filter(static fn($value) => !$predicate($value)); 187 | } 188 | 189 | /** 190 | * @return Either 191 | */ 192 | #[\NoDiscard] 193 | public function either(): Either 194 | { 195 | return $this->maybe->either(); 196 | } 197 | 198 | /** 199 | * @param callable(): \Throwable $error 200 | * 201 | * @return Attempt 202 | */ 203 | #[\NoDiscard] 204 | public function attempt(callable $error): Attempt 205 | { 206 | return $this->maybe->attempt($error); 207 | } 208 | 209 | /** 210 | * Force loading the value in memory (only useful for a deferred Maybe) 211 | * 212 | * @return self 213 | */ 214 | #[\NoDiscard] 215 | public function memoize(): self 216 | { 217 | return $this->maybe->memoize(); 218 | } 219 | 220 | /** 221 | * @return Sequence 222 | */ 223 | #[\NoDiscard] 224 | public function toSequence(): Sequence 225 | { 226 | return $this->maybe->toSequence(); 227 | } 228 | 229 | /** 230 | * @template V 231 | * 232 | * @param callable(T): self $just 233 | * @param callable(): self $nothing 234 | * 235 | * @return self 236 | */ 237 | #[\NoDiscard] 238 | public function eitherWay(callable $just, callable $nothing): self 239 | { 240 | return $this->maybe->eitherWay($just, $nothing); 241 | } 242 | } 243 | --------------------------------------------------------------------------------