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 theLtype and one for theRtype.Lis ofinttype, so the constructor withLis called. -
The
Leftvalue gets set to theintvalue passed which is 1. -
The
Rightvalue gets set to thedefault®which in this case is astringand the default value ofstringisnull.
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 theLtype and one for theRtype.Lin this case isint, so the constructor withRis called. -
The
Leftvalue gets set to hedefault(L)value which in this case is anintand the default value ofintis0. -
The
Rightvalue gets set to thestringvalue 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
intvalue 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
stringvalue 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
usingdeclaration. -
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
Rightvalue as a string. -
s ⇒ s.Lengthis the transformation function that converts thestringtoint. -
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
EitherExtensionsclass. -
Execute the function only for the
Rightvalue.
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
Rightvalue as a string. -
s ⇒ new Either<string, int>(s.Length)is the transformation function that converts thestringto 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);