immutable

#Either

This structure represent an alternative between 2 possibles types L and R (left and right), with a bias toward the right value. Usually this is used to represent Either, the valid value being on the right side benefit the right bias as map, flatMap and filter are done on the right value.

This technique is borrowed from the functional programming world.

For the examples below we will use the given imaginary functions:

use Innmind\Immutable\Either;

/**
 * @return Either
 */
function identify(ServerRequest $request): Either {
    if (/* some condition */) {
        return Either::right($theUser);
    }

    Either::left(new Error('User not found'));
}

/**
 * @return Either
 */
function accessResource(User $user): Either {
    if (/* check if user is allowed */) {
        return Either::right($resource);
    }

    return Either::left(new Error('User is not allowed'));
}

Note ServerRequest, User, Resource and Error are imaginary classes.

#::left()

This builds an Either instance with the given value in the left hand side.

$either = Either::left($anyValue);

Note usually this side is used for errors.

This builds an Either instance with the given value in the right hand side.

$either = Either::right($anyValue);

Note usually this side is used for valid values.

#::defer()

This is used to return an Either early with known data type but with the value that will be extracted from the callable when calling ->match(). The main use case is for IO operations.

$either = Either::defer(static function() {
    try {
        $value = /* wait for some IO operation like an http call */;

        return Either::right($value);
    } catch (\Throwable $e) {
        return Either::left($e);
    }
});

Methods called (except match) on a deferred Either will not be called immediately but will be composed to be executed once you call match.

Warning this means that if you never call match on a deferred Either it will do nothing.

#->map()

This will apply the map transformation on the right value if there is one, otherwise it's only a type change.

/** @var Either */
$either = identify($serverRequest)
/** @var Either */
$impersonated = $either->map(fn(User $user): Impersonated => $user->impersonateAdmin());

#->flatMap()

This is similar to ->map() but instead of returning the new right value you return a new Either object.

/** @var Either */
$either = identify($serverRequest);
/** @var Either */
$resource = $either->flatMap(fn(User $user): Either => accessResource($user));

#->match()

This is the only way to extract the wrapped value.

/** @var Response */
$response = identify($serverRequest)
    ->flatMap(fn(User $user): Either => accessResource($user))
    ->match(
        fn(Resource $resource) => new Response(200, $resource->toString()),
        fn(Error $error) => new Response(400, $error->message()), // here the error can be from identify or from accessResource
    );

Note Response is an imaginary class.

#->otherwise()

This is like ->flatMap() but is called when the instance contains a left value. The callable must return a new Either object.

/**
 * @return Either
 */
function identifyViaJsonPayload(ServerRequest $request): Either {
    if (/* find user from json payload */) {
        return Either::right($theUser);
    }

    return Either::left(new Error('User not found'));
}

/** @var Either */
$either = identify($request)
    ->otherwise(fn() => identifyViaJsonPayload($request));

#->filter()

Use this method to validate the right value when there is one. If the predicate doesn't accept the right value then it will return the value from the second callable as a left value.

identify($request)
    ->filter(
        fn(User $user): bool => $user->isAllowed(),
        fn() => new Error('User is not allowed'),
    )
    ->match(
        fn(User $user) => doSomething($user), // here we know the user is allowed
        fn(Error $error) => print($error->message()), // can be "User not found" or "User is not allowed"
    );

#->leftMap()

This is similar to the ->map() function but will be applied on the left value only.

/** @var Either */
$either = identify($request)
    ->leftMap(fn(Error $error) => new ErrorResponse($error));

#->maybe()

This returns a Maybe containing the right value, in case of a left value it returns a Maybe with nothing inside.

Either::right('something')->maybe()->match(
    static fn($value) => $value,
    static fn() => null,
); // returns 'something'
Either::left('something')->maybe()->match(
    static fn($value) => $value,
    static fn() => null,
); // returns null

#->memoize()

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

Either::defer(function() {
    return Either::right(\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,
    );

#->flip()

This method changes the side of the value contained in the Either. This is useful when you want to only keep the error and discard the right value you would use like this:

/**
 * @return Either
 */
function foo() { /*...*/}

$error = foo() // returns type Either
    ->flip() // returns type Either
    ->maybe(); // returns type Maybe

#->eitherWay()

This method is kind of combines both flatMap and otherwise in a single call. This is useful when you can't call otherwise after flatMap because you don't want to override the left value returned by flatMap.

/**
 * @return Either SideEffect when on macOS
 */
function isMac(): Either { /* ... */}
/**
 * @return Either
 */
function runMac(): Either { /* ... */ }
/**
 * @return Either
 */
function runLinux(): Either { /* ... */ }

$_ = isMac()->eitherWay(runMac(...), runLinux(...));

In this case we want to run runLinux only when isMac returns a SideEffect which is possible thanks to ->eitherWay(). Contrary to isMac()->flatMap(runMac(...))->otherwise(runLinux(...)) that could lead to runLinux to be called if runMac returns an error.