Skip to content

Instantly share code, notes, and snippets.

@flensrocker
Last active February 15, 2024 13:00
Show Gist options
  • Save flensrocker/49cadca3fa422c9cf3b5997d5e1915b5 to your computer and use it in GitHub Desktop.
Save flensrocker/49cadca3fa422c9cf3b5997d5e1915b5 to your computer and use it in GitHub Desktop.
Either monad in C#
Either<Exception, string?>[] eithers =
[
new Either<Exception, string?>.Right("Text"),
new Either<Exception, string?>.Right(null),
new Either<Exception, string?>.Left(new Exception("Oops!")),
];
var TryStringLength = EitherExtensions.TryInvoke<string?, int>(LegacyFunctions.StringLength);
foreach (var either in eithers)
{
var result = either
.SelectMany(TryStringLength)
.Match(
ex => $"Error: {ex.Message}",
len => $"Length is {len}"
);
// Oder mit LINQ:
// var result = (from e in either
// from r in TryStringLength(e)
// select r)
// .Match(
// ex => $"Error: {ex.Message}",
// len => $"Length is {len}"
// );
Console.WriteLine(result);
}
// Output:
// Length is 4
// Error: Value cannot be null. (Parameter 'text')
// Error: Oops!
public static class LegacyFunctions
{
public static int StringLength(this string? text)
{
ArgumentNullException.ThrowIfNull(text);
return text.Length;
}
}
public abstract class Either<TLeft, TRight>
{
private Either() { }
public abstract T Match<T>(Func<TLeft, T> onLeft, Func<TRight, T> onRight);
public sealed class Left(TLeft left) : Either<TLeft, TRight>
{
private readonly TLeft _left = left;
public override T Match<T>(Func<TLeft, T> onLeft, Func<TRight, T> onRight)
=> onLeft(_left);
}
public sealed class Right(TRight right) : Either<TLeft, TRight>
{
private readonly TRight _right = right;
public override T Match<T>(Func<TLeft, T> onLeft, Func<TRight, T> onRight)
=> onRight(_right);
}
}
public static class EitherExtensions
{
public static Either<TLeft2, TRight2> SelectBoth<TLeft1, TLeft2, TRight1, TRight2>(
this Either<TLeft1, TRight1> source,
Func<TLeft1, TLeft2> selectLeft,
Func<TRight1, TRight2> selectRight)
{
return source.Match<Either<TLeft2, TRight2>>(
onLeft: l => new Either<TLeft2, TRight2>.Left(selectLeft(l)),
onRight: r => new Either<TLeft2, TRight2>.Right(selectRight(r)));
}
public static Either<TLeft2, TRight> SelectLeft<TLeft1, TLeft2, TRight>(
this Either<TLeft1, TRight> source,
Func<TLeft1, TLeft2> selector)
{
return source.SelectBoth(selector, r => r);
}
public static Either<TLeft, TRight2> SelectRight<TLeft, TRight1, TRight2>(
this Either<TLeft, TRight1> source,
Func<TRight1, TRight2> selector)
{
return source.SelectBoth(l => l, selector);
}
public static Either<TLeft, TRight2> Select<TLeft, TRight1, TRight2>(
this Either<TLeft, TRight1> source,
Func<TRight1, TRight2> selector)
{
return source.SelectRight(selector);
}
public static Either<TLeft, TRight> Join<TLeft, TRight>(
this Either<TLeft, Either<TLeft, TRight>> source)
{
return source.Match(
onLeft: l => new Either<TLeft, TRight>.Left(l),
onRight: r => r);
}
public static Either<TLeft, TRight2> SelectMany<TLeft, TRight1, TRight2>(
this Either<TLeft, TRight1> source,
Func<TRight1, Either<TLeft, TRight2>> selector)
{
return source.Select(selector).Join();
}
public static Either<TLeft, TRight2> SelectMany<TLeft, TRight1, TRight2, U>(
this Either<TLeft, TRight1> source,
Func<TRight1, Either<TLeft, U>> k,
Func<TRight1, U, TRight2> s)
{
return source.SelectMany(x => k(x).Select(y => s(x, y)));
}
public static Func<TRight1, Either<Exception, TRight2>> TryInvoke<TRight1, TRight2>(Func<TRight1, TRight2> func)
{
return r1 =>
{
try
{
var r2 = func(r1);
return new Either<Exception, TRight2>.Right(r2);
}
catch (Exception ex)
{
return new Either<Exception, TRight2>.Left(ex);
}
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment