Either Type
Introduction
Sometimes we may need to return two separate types of values from a function, one type if the function executed successfully and one type if the function encountered errors - validation or exception.
The Either type can help with this.
The Either<Left,Right>
will return the Left
type for errors and the Right
type in case of no errors.
Something like this: Either<L,R> = Left(L) | Right R
-
Left(L)
- It wraps a value of typeL
, capturing details about the error. -
Right R
- It wraps a value of typeR
, capturing the success value.
The Left Type
Define the Left
type.
public static class Either
{
public struct Left<L>
{
internal L Value { get; }
internal Left(L value) { Value = value; }
public override string ToString() => $"Left({Value})";
}
}
A new Left
value can be created:-
var left = new Either.Left<string>("test");
Assert.Equal("test", left.Value);
The Right Type
Define the Right
type in the same static class Either
:
public struct Right<R>
{
internal R Value { get; }
internal Right(R value) { Value = value; }
public override string ToString() => $"Right({Value})";
}
A new Right
value can be created:-
var right = new Either.Right<string>("test");
Assert.Equal("test", right.Value);
The Either <L,R> Type
Now that we have the Left
and Right
type, we can move start defining the Either<L,R>
.
public struct Either<L, R>
{
internal L Left { get; }
internal R Right { get; }
private bool IsRight { get; }
private bool IsLeft => !IsRight;
internal Either(L left)
{
IsRight = false;
Left = left;
Right = default(R);
}
internal Either(R right)
{
IsRight = true;
Right = right;
Left = default(L);
}
}
Testing the left value:
Either<int, string> leftValue = new Either<int, string>(1); (1)
Assert.Equal(1, leftValue.Left); (2)
Assert.Null(leftValue.Right); (3)
-
The
Either<L,R>
type has 2 constructors:- one for theL
type and one for theR
type.L
is ofint
type, so the constructor withL
is called. -
The
Left
value gets set to theint
value passed which is 1. -
The
Right
value gets set to thedefault®
which in this case is astring
and the default value ofstring
isnull
.
Testing the right value:
Either<int, string> rightValue = new Either<int, string>("test"); (1)
Assert.Equal(0, rightValue.Left); (2)
Assert.Equal("test", rightValue.Right); (3)
-
The
Either<L,R>
type has 2 constructors:- one for theL
type and one for theR
type.L
in this case isint
, so the constructor withR
is called. -
The
Left
value gets set to hedefault(L)
value which in this case is anint
and the default value ofint
is0
. -
The
Right
value gets set to thestring
value passed which is "test".
The Implicit Operators
Define the implicit operators that make it convenient to convert an L
or R
type to an Either<L,R>
type.
public static implicit operator Either<L, R>(L left) => new(left);
public static implicit operator Either<L, R>(R right) => new(right);
public static implicit operator Either<L, R>(Either.Left<L> left) => new(left.Value);
public static implicit operator Either<L, R>(Either.Right<R> right) => new(right.Value);
Testing the implicit operators:
Left value tests:
Either<int, string> leftValue1 = 1; (1)
Either<int, string> leftValue2 = new Either.Left<int>(1); (2)
Assert.Equal(1, leftValue1.Left);
Assert.Null(leftValue1.Right);
Assert.Equal(1, leftValue2.Left);
Assert.Null(leftValue2.Right);
-
Converts the
int
value toEither<int, string>
. -
Converts the
Either.Left<int>
toEither<int,string>
value.
Right value tests:
Either<int, string> rightValue1 = "test"; (1)
Either<int, string> rightValue2 = new Either.Right<string>("test"); (2)
Assert.Equal("test", rightValue1.Right);
Assert.Equal(0, rightValue1.Left);
Assert.Equal("test", rightValue2.Right);
Assert.Equal(0, rightValue2.Left);
-
Converts the
string
value toEither<int,string>
. -
Converts the
Either.Right<string>
toEither<int,string>
value.
The F static class
It is a bit annoying to type new Either.Right<string>("test")
and new Either.Left<int>(1)
to create a Left and Right type.
It is better to type Left(1)
and Right("test")
to create the Either<L,R>
type.
The F
static class can help.
public static partial class F
{
public static Either.Left<L> Left<L>(L l) => new(l);
public static Either.Right<R> Right<R>(R r) => new(r);
}
Now by importing the static class, we can use these functions as follows:
using static F; (1)
var left = Left<int>(0); (2)
var right = Right<string>("test"); (3)
Assert.Equal(0, left.Value);
Assert.Equal("test", right.Value);
-
The
using
declaration. -
Creates an
Either.Left<int>
type. -
Creates an
Either.Right<string>
type.
The Match method
This is the all important Match
method of Either<L,R>
.
We will use this function to read the Left
and Right
values of the Either<L,R>
type.
There is no other way the caller can read the Left
and Right
values but through the Match
method.
This forces the caller to handle both the cases.
Lets add the method to the Either<L,R>
type.
public TR Match<TR>(Func<L, TR> left, Func<R, TR> right)
=> IsLeft ? left(this.Left) : right(this.Right);
Testing the method when the Left
value is passed:
Either<int, string> leftValue = Left<int>(1);
var returnValue = leftValue.Match(
left: l => $"Value is {l}",
right: r => $"Value is {r}"
);
Assert.Equal("Value is 1", returnValue);
Testing the method when the Right
value is passed:
Either<int, string> rightValue = Right<string>("test");
var returnValue = rightValue.Match(
left: l => $"Value is {l}",
right: r => $"Value is {r}"
);
Assert.Equal("Value is test", returnValue);
The Map/Select function
Now suppose we would like to transform the Left
or Right
value of Either<L,R>
by passing it to a transform function T → R
Here is an example:
Either<int, string> testMap = Right<string>("test"); (1)
var count = testMap
.Map(s => s.Length) (2)
.Match(
left: l => l,
right: r => r
);
Assert.Equal(4, count); (3)
-
Define a
Right
value as a string. -
s ⇒ s.Length
is the transformation function that converts thestring
toint
. -
Assert the value is 4.
Note that we transform only the Right
value and not the left
one.
This is done because we want to treat the`Left` value as the value containing the error data.
We do not want to do any transformation on the error data but just pass it along straight to the Match
function at the end.
This is Railway Oriented Programming.
How do we write such a function:-
public static class EitherExtensions (1)
{
public static Either<L, RR> Map<L, R, RR>(this Either<L, R> @this, Func<R, RR> f)
=> @this.Match<Either<L, RR>>(
l => F.Left(l),
r => F.Right(f(r)) (2)
);
}
-
Create the
EitherExtensions
class. -
Execute the function only for the
Right
value.
In LINQ, the word used for Map
is Select
.
Select
can be defined in terms of Map
as follows:
public static Either<L, R> Select<L, T, R>(this Either<L, T> @this, Func<T, R> map)
=> @this.Map(map);
Example rewritten using Select
:
Either<int, string> testMap = Right<string>("test");
var count = testMap
.Select(s => s.Length)
.Match(
left: l => l,
right: r => r
);
Assert.Equal(4, count);
The Bind/SelectMany function
Now suppose we have like to transform the Left
or Right
value of Either<L,R>
by passing it to a transform function that takes a normal value and returns an Either. T → Either<L,R>
Here is an example:
Either<string, string> testMap = Right<string>("test");
var count = testMap
.Bind(s => new Either<string, int>(s.Length))
.Match(
left: l => 0,
right: r => r
);
Assert.Equal(4, count);
-
Define a
Right
value as a string. -
s ⇒ new Either<string, int>(s.Length)
is the transformation function that converts thestring
to anEither<string,int>
. -
Assert the value is 4.
Again, we do not want to process the Left
value, just the Right
.
Here is the bind function:
public static Either<L, RR> Bind<L, R, RR>(this Either<L, R> @this, Func<R, Either<L, RR>> f)
=> @this.Match(
l => F.Left(l),
r => f(r)
);
In LINQ, the word used for Bind
is SelectMany
.
SelectMany
can be defined in terms of Bind
as follows:
public static Either<L, RR> SelectMany<L, R, RR>(this Either<L, R> @this, Func<R, Either<L, RR>> bind)
=> @this.Bind(bind);
Example rewritten using SelectMany
:
Either<string, string> testMap = Right<string>("test");
var count = testMap
.Bind(s => new Either<string, int>(s.Length))
.Match(
left: l => 0,
right: r => r
);
Assert.Equal(4, count);