Skip to content

Attempt

This structures is similar to Either 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:

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

You will rarely use this method directly.

::result()

This builds an Attempt that succeeded with the given value:

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

You will rarely use this method directly.

::of()

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

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

    return $anyValue;
});

This is the equivalent of:

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

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

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() or ->match() is called.

$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.

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

Here $attempt contains 1;

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

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

->flatMap()

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

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

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

->match()

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

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

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

->recover()

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

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

Here $attempt is 42 because the first Attempt raised a DivisionByZeroError.

->maybe()

This converts an Attempt to a Maybe.

Attempt::result($value)->maybe();
// is the same as
Maybe::just($value);
Attempt::error(new \Exception)->maybe()
// is the same as
Maybe::nothing();

->either()

This converts an Attempt to a Either.

Attempt::result($value)->either();
// is the same as
Either::right($value);
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.

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.

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

Here $result is necessarily a fraction of 1 but this code may raise the DivisionByZeroError exception.