Skip to content

Commit ee97ef4

Browse files
committed
Merge branch 'develop'
* develop: specify next release use psalm 5 fix tests add Fold add documentation allow to map results and failures add fold monad
2 parents 2e8df7e + 7ed3de8 commit ee97ef4

25 files changed

+904
-8
lines changed

.journal

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ return static function(Config $config): Config
5151
'State',
5252
Path::of('STATE.md'),
5353
),
54+
Entry::markdown(
55+
'Fold',
56+
Path::of('FOLD.md'),
57+
),
5458
Entry::markdown(
5559
'Monoids',
5660
Path::of('MONOIDS.md'),

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 4.11.0 - 2023-02-18
4+
5+
### Added
6+
7+
- `Innmind\Immutable\Fold`
8+
39
## 4.10.0 - 2023-02-05
410

511
### Added

composer.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,12 @@
3131
},
3232
"require-dev": {
3333
"phpunit/phpunit": "~9.0",
34-
"vimeo/psalm": "~4.28",
34+
"vimeo/psalm": "~5.6",
3535
"innmind/black-box": "^4.4",
3636
"innmind/coding-standard": "~2.0"
3737
},
3838
"conflict": {
39-
"innmind/black-box": "<4.4|~5.0",
40-
"vimeo/psalm": "4.9.3"
39+
"innmind/black-box": "<4.4|~5.0"
4140
},
4241
"suggest": {
4342
"innmind/black-box": "For property based testing"

docs/FOLD.md

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# `Fold`
2+
3+
The `Fold` monad is intented to work with _(infinte) 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.
4+
5+
An example is reading from a socket as it's an infinite stream of strings:
6+
7+
```php
8+
$socket = \stream_socket_client(/* args */);
9+
/** @var Fold<string, list<string>, list<string>> */
10+
$fold = Fold::with([]);
11+
12+
do {
13+
// production code should wait for the socket to be "ready"
14+
$line = \fgets($socket);
15+
16+
if ($line === false) {
17+
$fold = Fold::fail('socket not readable');
18+
}
19+
20+
$fold = $fold
21+
->map(static fn($lines) => \array_merge($lines, [$line]))
22+
->flatMap(static fn($lines) => match (\end($lines)) {
23+
"quit\n" => Fold::result($lines),
24+
default => Fold::with($lines),
25+
});
26+
$continue = $fold->match(
27+
static fn() => true, // still folding
28+
static fn() => false, // got a result so stop
29+
static fn() => false, // got a failure so stop
30+
);
31+
} while ($continue);
32+
33+
$fold->match(
34+
static fn() => null, // unreachable in this case because no more folding outside the loop
35+
static fn($lines) => \var_dump($lines),
36+
static fn($failure) => throw new \RuntimeException($failure),
37+
);
38+
```
39+
40+
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')`.
41+
42+
## `::with()`
43+
44+
This named constructor accepts a value with the notion that more elements are necessary to compute a result
45+
46+
## `::result()`
47+
48+
This named constructor accepts a _result_ value meaning that folding is finished.
49+
50+
## `::fail()`
51+
52+
This named constructor accepts a _failure_ value meaning that the folding operation failed and no _result_ will be reachable.
53+
54+
## `->map()`
55+
56+
This method allows to transform the value being folded.
57+
58+
```php
59+
$fold = Fold::with([])->map(static fn(array $folding) => new \ArrayObject($folding));
60+
```
61+
62+
## `->flatMap()`
63+
64+
This method allows to both change the value and the _state_, for example switching from _folding_ to _result_.
65+
66+
```php
67+
$someElement = /* some data */;
68+
$fold = Fold::with([])->flatMap(static fn($elements) => match ($someElement) {
69+
'finish' => Fold::result($elements),
70+
default => Fold::with(\array_merge($elements, [$someElement])),
71+
});
72+
```
73+
74+
## `->mapResult()`
75+
76+
Same as [`->map()`](#map) except that it will transform the _result_ value when there is one.
77+
78+
## `->mapFailure()`
79+
80+
Same as [`->map()`](#map) except that it will transform the _failure_ value when there is one.
81+
82+
## `->maybe()`
83+
84+
This will return the _terminal_ value of the folding, meaning either a _result_ or a _failure_.
85+
86+
```php
87+
Fold::with([])->maybe()->match(
88+
static fn() => null, // not called as still folding
89+
static fn() => doStuff(), // called as it is still folding
90+
);
91+
Fold::result([])->maybe()->match(
92+
static fn($either) => $either->match(
93+
static fn($result) => $result, // the value here is the array passed to ::result() above
94+
static fn() => null, // not called as it doesn't contain a failure
95+
),
96+
static fn() => null, // not called as we have a result
97+
);
98+
Fold::fail('some error')->maybe()->match(
99+
static fn($either) => $either->match(
100+
static fn() => null, // not called as we have a failure
101+
static fn($error) => var_dump($error), // the value here is the string passed to ::fail() above
102+
),
103+
static fn() => null, // not called as we have a result
104+
);
105+
```
106+
107+
## `->match()`
108+
109+
This method allows to extract the value contained in the object.
110+
111+
```php
112+
Fold::with([])->match(
113+
static fn($folding) => doStuf($folding), // value from ::with()
114+
static fn() => null, // not called
115+
static fn() => null, // not called
116+
);
117+
Fold::result([])->match(
118+
static fn() => null, // not called
119+
static fn($result) => doStuf($result), // value from ::result()
120+
static fn() => null, // not called
121+
);
122+
Fold::fail('some error')->match(
123+
static fn() => null, // not called
124+
static fn() => null, // not called
125+
static fn($error) => doStuf($error), // value from ::fail()
126+
);
127+
```

docs/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This library provides the 7 following structures:
2020
- [`Maybe`](MAYBE.md)
2121
- [`Either`](EITHER.md)
2222
- [`State`](STATE.md)
23+
- [`Fold`](FOLD.md)
2324

2425
See the documentation for each structure to understand how to use them.
2526

psalm.xml

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<?xml version="1.0"?>
22
<psalm
33
errorLevel="1"
4+
findUnusedCode="false"
5+
findUnusedBaselineEntry="true"
46
resolveFromConfigFile="true"
57
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
68
xmlns="https://getpsalm.org/schema/config"
@@ -12,4 +14,7 @@
1214
<directory name="vendor" />
1315
</ignoreFiles>
1416
</projectFiles>
17+
<disableExtensions>
18+
<extension name="random" />
19+
</disableExtensions>
1520
</psalm>

src/Accumulate.php

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*
1010
* @template T
1111
* @template S
12+
* @implements \Iterator<T, S>
1213
* @internal Do not use this in your code
1314
* @psalm-immutable Not really immutable but to simplify declaring immutability of other structures
1415
*/

src/Fold.php

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Innmind\Immutable;
5+
6+
use Innmind\Immutable\Fold\{
7+
Implementation,
8+
With,
9+
Result,
10+
Failure,
11+
};
12+
13+
/**
14+
* @template F Failure
15+
* @template R Result
16+
* @template C Computation
17+
* @psalm-immutable
18+
*/
19+
final class Fold
20+
{
21+
private Implementation $fold;
22+
23+
private function __construct(Implementation $fold)
24+
{
25+
$this->fold = $fold;
26+
}
27+
28+
/**
29+
* @psalm-pure
30+
*
31+
* @template T
32+
* @template U
33+
* @template V
34+
*
35+
* @param V $value
36+
*
37+
* @return self<T, U, V>
38+
*/
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<T, U, V>
54+
*/
55+
public static function result(mixed $result): self
56+
{
57+
return new self(new Result($result));
58+
}
59+
60+
/**
61+
* @psalm-pure
62+
*
63+
* @template T
64+
* @template U
65+
* @template V
66+
*
67+
* @param T $failure
68+
*
69+
* @return self<T, U, V>
70+
*/
71+
public static function fail(mixed $failure): self
72+
{
73+
return new self(new Failure($failure));
74+
}
75+
76+
/**
77+
* @template A
78+
*
79+
* @param callable(C): A $map
80+
*
81+
* @return self<F, R, A>
82+
*/
83+
public function map(callable $map): self
84+
{
85+
return new self($this->fold->map($map));
86+
}
87+
88+
/**
89+
* @template T
90+
* @template U
91+
* @template V
92+
*
93+
* @param callable(C): self<T, U, V> $map
94+
*
95+
* @return self<F|T, R|U, V>
96+
*/
97+
public function flatMap(callable $map): self
98+
{
99+
return $this->fold->flatMap($map);
100+
}
101+
102+
/**
103+
* @template A
104+
*
105+
* @param callable(R): A $map
106+
*
107+
* @return self<F, A, C>
108+
*/
109+
public function mapResult(callable $map): self
110+
{
111+
return new self($this->fold->mapResult($map));
112+
}
113+
114+
/**
115+
* @template A
116+
*
117+
* @param callable(F): A $map
118+
*
119+
* @return self<A, R, C>
120+
*/
121+
public function mapFailure(callable $map): self
122+
{
123+
return new self($this->fold->mapFailure($map));
124+
}
125+
126+
/**
127+
* @return Maybe<Either<F, R>>
128+
*/
129+
public function maybe(): Maybe
130+
{
131+
return $this->fold->maybe();
132+
}
133+
134+
/**
135+
* @template T
136+
*
137+
* @param callable(C): T $with
138+
* @param callable(R): T $result
139+
* @param callable(F): T $failure
140+
*
141+
* @return T
142+
*/
143+
public function match(
144+
callable $with,
145+
callable $result,
146+
callable $failure,
147+
): mixed {
148+
return $this->fold->match($with, $result, $failure);
149+
}
150+
}

0 commit comments

Comments
 (0)