λ1011: Prefer Select over Match

A Match that keeps the failure state and transforms the value should be expressed as Select.

Cause

Match is called on an Option<T>, Either<L, R> or Result<T> where the failure branch returns the monad's own empty or error state (Option<TResult>.None, Either<L, TResult>.Left, Result<TResult>.Error) and the success branch returns a plain value of the inner type.

When the success branch returns a monad itself, the call re-implements SelectMany and λ1007 reports it instead.

Reason for rule

Match is the most general way to leave a monad; it is the escape hatch that every other operation could be written with. When the failure branch merely passes the failure state on and the success branch maps the value, the call re-implements Select. Using the dedicated method names the intent, drops the repeated failure branch and keeps the type parameter inferred.

How to fix violations

Replace the Match call with Select, passing the former some/right/ok argument. A code fix is available.

Examples

Disallowed

static Option<string> Example(Option<int> option)
    => option.Match(none: Option<string>.None, some: x => x.ToString());

static Either<string, int> Example(Either<string, int> either)
    => either.Match(left: Either<string, int>.Left, right: x => x * 2);

static Result<string> Example(Result<int> result)
    => result.Match(error: Result<string>.Error, ok: x => x.ToString());

Allowed

static Option<string> Example(Option<int> option)
    => option.Select(x => x.ToString());

static Either<string, int> Example(Either<string, int> either)
    => either.Select(x => x * 2);

static Result<string> Example(Result<int> result)
    => result.Select(x => x.ToString());

// The failure branch produces a value, so this is a legitimate Match.
static Option<int> Example(Option<int> option, Option<int> fallback)
    => option.Match(none: fallback, some: x => x + 10);