Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  specify next release
  use psalm 5
  fix tests
  add Fold
  add documentation
  allow to map results and failures
  add fold monad
  • Loading branch information
Baptouuuu committed Feb 18, 2023
2 parents 2e8df7e + 7ed3de8 commit ee97ef4
Show file tree
Hide file tree
Showing 25 changed files with 904 additions and 8 deletions.
4 changes: 4 additions & 0 deletions .journal
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ return static function(Config $config): Config
'State',
Path::of('STATE.md'),
),
Entry::markdown(
'Fold',
Path::of('FOLD.md'),
),
Entry::markdown(
'Monoids',
Path::of('MONOIDS.md'),
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 4.11.0 - 2023-02-18

### Added

- `Innmind\Immutable\Fold`

## 4.10.0 - 2023-02-05

### Added
Expand Down
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@
},
"require-dev": {
"phpunit/phpunit": "~9.0",
"vimeo/psalm": "~4.28",
"vimeo/psalm": "~5.6",
"innmind/black-box": "^4.4",
"innmind/coding-standard": "~2.0"
},
"conflict": {
"innmind/black-box": "<4.4|~5.0",
"vimeo/psalm": "4.9.3"
"innmind/black-box": "<4.4|~5.0"
},
"suggest": {
"innmind/black-box": "For property based testing"
Expand Down
127 changes: 127 additions & 0 deletions docs/FOLD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# `Fold`

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.

An example is reading from a socket as it's an infinite stream of strings:

```php
$socket = \stream_socket_client(/* args */);
/** @var Fold<string, list<string>, list<string>> */
$fold = Fold::with([]);

do {
// production code should wait for the socket to be "ready"
$line = \fgets($socket);

if ($line === false) {
$fold = Fold::fail('socket not readable');
}

$fold = $fold
->map(static fn($lines) => \array_merge($lines, [$line]))
->flatMap(static fn($lines) => match (\end($lines)) {
"quit\n" => Fold::result($lines),
default => Fold::with($lines),
});
$continue = $fold->match(
static fn() => true, // still folding
static fn() => false, // got a result so stop
static fn() => false, // got a failure so stop
);
} while ($continue);

$fold->match(
static fn() => null, // unreachable in this case because no more folding outside the loop
static fn($lines) => \var_dump($lines),
static fn($failure) => throw new \RuntimeException($failure),
);
```

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')`.

## `::with()`

This named constructor accepts a value with the notion that more elements are necessary to compute a result

## `::result()`

This named constructor accepts a _result_ value meaning that folding is finished.

## `::fail()`

This named constructor accepts a _failure_ value meaning that the folding operation failed and no _result_ will be reachable.

## `->map()`

This method allows to transform the value being folded.

```php
$fold = Fold::with([])->map(static fn(array $folding) => new \ArrayObject($folding));
```

## `->flatMap()`

This method allows to both change the value and the _state_, for example switching from _folding_ to _result_.

```php
$someElement = /* some data */;
$fold = Fold::with([])->flatMap(static fn($elements) => match ($someElement) {
'finish' => Fold::result($elements),
default => Fold::with(\array_merge($elements, [$someElement])),
});
```

## `->mapResult()`

Same as [`->map()`](#map) except that it will transform the _result_ value when there is one.

## `->mapFailure()`

Same as [`->map()`](#map) except that it will transform the _failure_ value when there is one.

## `->maybe()`

This will return the _terminal_ value of the folding, meaning either a _result_ or a _failure_.

```php
Fold::with([])->maybe()->match(
static fn() => null, // not called as still folding
static fn() => doStuff(), // called as it is still folding
);
Fold::result([])->maybe()->match(
static fn($either) => $either->match(
static fn($result) => $result, // the value here is the array passed to ::result() above
static fn() => null, // not called as it doesn't contain a failure
),
static fn() => null, // not called as we have a result
);
Fold::fail('some error')->maybe()->match(
static fn($either) => $either->match(
static fn() => null, // not called as we have a failure
static fn($error) => var_dump($error), // the value here is the string passed to ::fail() above
),
static fn() => null, // not called as we have a result
);
```

## `->match()`

This method allows to extract the value contained in the object.

```php
Fold::with([])->match(
static fn($folding) => doStuf($folding), // value from ::with()
static fn() => null, // not called
static fn() => null, // not called
);
Fold::result([])->match(
static fn() => null, // not called
static fn($result) => doStuf($result), // value from ::result()
static fn() => null, // not called
);
Fold::fail('some error')->match(
static fn() => null, // not called
static fn() => null, // not called
static fn($error) => doStuf($error), // value from ::fail()
);
```
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This library provides the 7 following structures:
- [`Maybe`](MAYBE.md)
- [`Either`](EITHER.md)
- [`State`](STATE.md)
- [`Fold`](FOLD.md)

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

Expand Down
5 changes: 5 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
findUnusedCode="false"
findUnusedBaselineEntry="true"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
Expand All @@ -12,4 +14,7 @@
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<disableExtensions>
<extension name="random" />
</disableExtensions>
</psalm>
1 change: 1 addition & 0 deletions src/Accumulate.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
* @template T
* @template S
* @implements \Iterator<T, S>
* @internal Do not use this in your code
* @psalm-immutable Not really immutable but to simplify declaring immutability of other structures
*/
Expand Down
150 changes: 150 additions & 0 deletions src/Fold.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php
declare(strict_types = 1);

namespace Innmind\Immutable;

use Innmind\Immutable\Fold\{
Implementation,
With,
Result,
Failure,
};

/**
* @template F Failure
* @template R Result
* @template C Computation
* @psalm-immutable
*/
final class Fold
{
private Implementation $fold;

private function __construct(Implementation $fold)
{
$this->fold = $fold;
}

/**
* @psalm-pure
*
* @template T
* @template U
* @template V
*
* @param V $value
*
* @return self<T, U, V>
*/
public static function with(mixed $value): self
{
return new self(new With($value));
}

/**
* @psalm-pure
*
* @template T
* @template U
* @template V
*
* @param U $result
*
* @return self<T, U, V>
*/
public static function result(mixed $result): self
{
return new self(new Result($result));
}

/**
* @psalm-pure
*
* @template T
* @template U
* @template V
*
* @param T $failure
*
* @return self<T, U, V>
*/
public static function fail(mixed $failure): self
{
return new self(new Failure($failure));
}

/**
* @template A
*
* @param callable(C): A $map
*
* @return self<F, R, A>
*/
public function map(callable $map): self
{
return new self($this->fold->map($map));
}

/**
* @template T
* @template U
* @template V
*
* @param callable(C): self<T, U, V> $map
*
* @return self<F|T, R|U, V>
*/
public function flatMap(callable $map): self
{
return $this->fold->flatMap($map);
}

/**
* @template A
*
* @param callable(R): A $map
*
* @return self<F, A, C>
*/
public function mapResult(callable $map): self
{
return new self($this->fold->mapResult($map));
}

/**
* @template A
*
* @param callable(F): A $map
*
* @return self<A, R, C>
*/
public function mapFailure(callable $map): self
{
return new self($this->fold->mapFailure($map));
}

/**
* @return Maybe<Either<F, R>>
*/
public function maybe(): Maybe
{
return $this->fold->maybe();
}

/**
* @template T
*
* @param callable(C): T $with
* @param callable(R): T $result
* @param callable(F): T $failure
*
* @return T
*/
public function match(
callable $with,
callable $result,
callable $failure,
): mixed {
return $this->fold->match($with, $result, $failure);
}
}
Loading

0 comments on commit ee97ef4

Please sign in to comment.