Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  specify next release
  fix psalm errors
  add Attempt::unwrap()
  add Attempt monad
  add Sequence::sink()
  fix Either and Maybe ::memoize() not loading everything
  • Loading branch information
Baptouuuu committed Dec 1, 2024
2 parents 6bef242 + ea05f5e commit 5c5aa75
Show file tree
Hide file tree
Showing 23 changed files with 1,747 additions and 2 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 5.11.0 - 2024-12-01

### Added

- `Innmind\Immutable\Sequence::sink()`
- `Innmind\Immutable\Attempt`

### Fixed

- `Innmind\Immutable\Maybe::memoize()` and `Innmind\Immutable\Either::memoize()` was only unwrapping the first layer of the monad. It now recursively unwraps until all the deferred monads are memoized.

## 5.10.0 - 2024-11-09

### Added
Expand Down
197 changes: 197 additions & 0 deletions docs/structures/attempt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# `Attempt`

This structures is similar to [`Either`](either.md) but where the left value is necessarily an instance of `\Throwable`.

Its main use is as a return type of any function that would normally throw an exception. Instead of throwing and let the exception bubble up the call stack, it's caught in the structure and forces you to deal with this exception at some point.

Unlike an `Either` the error type can't be more precise than `\Throwable`.

`Attempt` is intended to be used as a return type where the call may fail but you can't know in advance all the possible failing scenarii. This is the case for interfaces where the kind of error will depend on the implementation details.

If you already know all the possible failing scenarii you should use an `Either` instead.

??? note
In other languages this monad is called `Try`. But this is a reserved keyword in PHP, hence the name `Attempt`.

## `::error()`

This builds an `Attempt` that failed with the given exception:

```php
$attempt = Attempt::error(new \Exception);
```

!!! note ""
You will rarely use this method directly.

## `::result()`

This builds an `Attempt` that succeeded with the given value:

```php
$attempt = Attempt::result($anyValue);
```

!!! note ""
You will rarely use this method directly.

## `::of()`

This builds an `Attempt` that will immediately call the callable and catch any exception:

```php
$attempt = Attempt::of(static function() {
if (/* some condition */) {
throw new \Exception;
}

return $anyValue;
});
```

This is the equivalent of:

```php
$doStuff = static function() {
if (/* some condition */) {
return Attempt::error(new \Exception);
}

return Attempt::result($anyValue);
};
$attempt = $doStuff();
```

!!! success ""
This is very useful to wrap any third party code to a monadic style.

## `::defer()`

This builds an `Attempt` where the callable passed will be called only when [`->memoize()`](#-memoize) or [`->match()`](#-match) is called.

```php
$attempt = Attempt::defer(static fn() => Attempt::of(doStuff(...)));
// doStuff has not been called yet
$attempt->memoize();
// doStuff has been called
```

The main use case is for IO operations.

## `->map()`

This will apply the map transformation on the result if no previous error occured.

=== "Result"
```php
$attempt = Attempt::of(static fn() => 1/2)
->map(static fn(int $i) => $i*2);
```

Here `#!php $attempt` contains `1`;

=== "Error"
```php
$attempt = Attempt::of(static fn() => 1/0)
->map(static fn(int $i) => $i*2);
```

Here `#!php $attempt` contains a `DivisionByZeroError` and the callable passed to `map` has not been called.

## `->flatMap()`

This is similar to `#!php ->map()` except the callable passed to it must return an `Attempt` indicating that it may fail.

```php
$attempt = Attempt::result(2 - $reduction)
->flatMap(static fn(int $divisor) => Attempt::of(
static fn() => 42 / $divisor,
));
```

If `#!php $reduction` is `#!php 2` then `#!php $attempt` will contain a `DivisionByZeroError` otherwise for any other value it will contain a fraction of `#!php 42`.

## `->match()`

This extracts the result value but also forces you to deal with any potential error.

```php
$result = Attempt::of(static fn() => 2 / $reduction)->match(
static fn($fraction) => $fraction,
static fn(\Throwable $e) => $e,
);
```

If `#!php $reduction` is `#!php 0` then `#!php $result` will be an instance of `DivisionByZeroError`, otherwise it will be a fraction of `#!php 2`.

## `->recover()`

This will allow you to recover in case of a previous error.

```php
$attempt = Attempt::of(static fn() => 1/0)
->recover(static fn(\Throwable $e) => Attempt::result(42));
```

Here `#!php $attempt` is `#!php 42` because the first `Attempt` raised a `DivisionByZeroError`.

## `->maybe()`

This converts an `Attempt` to a `Maybe`.

=== "Result"
```php
Attempt::result($value)->maybe();
// is the same as
Maybe::just($value);
```

=== "Error"
```php
Attempt::error(new \Exception)->maybe()
// is the same as
Maybe::nothing();
```

## `->either()`

This converts an `Attempt` to a `Either`.

=== "Result"
```php
Attempt::result($value)->either();
// is the same as
Either::right($value);
```

=== "Error"
```php
Attempt::error(new \Exception)->either()
// is the same as
Either::left(new \Exception);
```

## `->memoize()`

This method force to load the contained value into memory. This is only useful for a deferred `Attempt`, this will do nothing for other attempts as the value is already known.

```php
Attempt::defer(static fn() => Attempt::result(\rand()))
->map(static fn($i) => $i * 2) // value still not loaded here
->memoize() // call the rand function and then apply the map and store it in memory
->match(
static fn($i) => doStuff($i),
static fn() => null,
);
```

## `->unwrap()`

This will return the result or throw any previous error.

```php
$result = Attempt::of(static fn() => 1 / $divisor)
->unwrap();
```

Here `#!php $result` is necessarily a fraction of `#!php 1` but this code may raise the `DivisionByZeroError` exception.
1 change: 1 addition & 0 deletions docs/structures/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This library provides the following structures:
- [`RegExp`](regexp.md)
- [`Maybe`](maybe.md)
- [`Either`](either.md)
- [`Attempt`](attempt.md)
- [`Validation`](validation.md)
- [`Identity`](identity.md)
- [`State`](state.md)
Expand Down
74 changes: 74 additions & 0 deletions docs/structures/sequence.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,80 @@ $sum = $sequence->reduce(0, fn($sum, $int) => $sum + $int);
$sum; // 10
```

### `->sink()` :material-memory-arrow-down:

This is similar to [`->reduce`](#-reduce) except you decide on each iteration it you want to continue reducing or not.

This is useful for long sequences (mainly lazy ones) where you need to reduce until you find some value in the `Sequence` or the reduced value matches some condition. This avoids iterating over values you know for sure you won't need.

=== "By hand"
```php
use Innmind\Immutable\Sequence\Sink\Continuation;

$sequence = Sequence::of(1, 2, 3, 4, 5);
$sum = $sequence
->sink(0)
->until(static fn(
int $sum,
int $i,
Continuation $continuation,
) => match (true) {
$sum > 5 => $continuation->stop($sum),
default => $continuation->continue($sum + $i),
});
```

Here `#!php $sum` is `#!php 6` and the `Sequence` stopped iterating on the 4th value.

=== "Maybe"
```php
$sequence = Sequence::of(1, 2, 3, 4, 5);
$sum = $sequence
->sink(0)
->maybe(static fn(int $sum, int $i) => match (true) {
$sum > 5 => Maybe::nothing(),
default => Maybe::just($sum + $i),
})
->match(
static fn(int $sum) => $sum,
static fn() => null,
);
```

Instead of manually specifying if we want to continue or not, it's inferred by the content of the `Maybe`.

Here the `#!php $sum` is `#!php null` because on the 4th iteration we return a `#!php Maybe::nothing()`.

!!! warning ""
Bear in mind that the carried value is lost when an iteration returns `#!php Maybe::nothing()`.

If you need to still have access to the carried value you should use `#!php ->sink()->either()` and place the carried value on the left side.

??? abstract
In essence this allows the transformation of `Sequence<Maybe<T>>` to `Maybe<Sequence<T>>`.

=== "Either"
```php
$sequence = Sequence::of(1, 2, 3, 4, 5);
$sum = $sequence
->sink(0)
->either(static fn(int $sum, int $i) => match (true) {
$sum > 5 => Either::left($sum),
default => Either::right($sum + $i),
})
->match(
static fn(int $sum) => $sum,
static fn(int $sum) => $sum,
);
```

Instead of manually specifying if we want to continue or not, it's inferred by the content of the `Either`.

Here the `#!php $sum` is `#!php 6` because on the 4th iteration we return an `#!php Either::left()` with the carried sum from the previous iteration.

??? abstract
In essence this allows the transformation of `Sequence<Either<E, T>>` to `Either<E, Sequence<T>>`.

## Misc.

### `->equals()` :material-memory-arrow-down:
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ nav:
- RegExp: structures/regexp.md
- Maybe: structures/maybe.md
- Either: structures/either.md
- Attempt: structures/attempt.md
- Validation: structures/validation.md
- Identity: structures/identity.md
- State: structures/state.md
Expand Down
Loading

0 comments on commit 5c5aa75

Please sign in to comment.