Last active
March 27, 2025 23:21
-
-
Save cartercanedy/b4c20200e79eb693caefdcc6791633d3 to your computer and use it in GitHub Desktop.
C# Result<TOk, TErr> to simulate a basic discriminated union until they get here :)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
global using static Results.Result; | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Runtime.CompilerServices; | |
namespace Results; | |
/// <summary> | |
/// Covariant interface for <see cref="Result{TOk, TErr}"/> that assists in consumer variable binding. | |
/// <br/> | |
/// This contract implies that the interface will not throw an exception, instead allowing for happy-path error handling. | |
/// <br/> | |
/// </summary> | |
/// <typeparam name="T">The type returned by a successful call.</typeparam> | |
/// <typeparam name="E">The exception type that is returned on a failed call.</typeparam> | |
public interface IResult<out T, out E> { | |
/// <summary> | |
/// Returns <typeparamref name="T"/> if bound to valid data.<br/> | |
/// Otherwise, throws <see cref="UnwrapError{E}"/> with the <typeparamref name="E"/> instance that was contained. | |
/// </summary> | |
/// <exception cref="UnwrapError{E}"/> | |
T Unwrap(); | |
/// <summary> | |
/// Returns <typeparamref name="E"/> if bound to valid error data.<br/> | |
/// Otherwise, throws <see cref="UnwrapError{T}"/> with the <typeparamref name="T"/> instance that was contained. | |
/// </summary> | |
/// <exception cref="UnwrapError{T}"/> | |
E UnwrapErr(); | |
/// <summary> | |
/// Returns <typeparamref name="T"/> if bound to valid data.<br/> | |
/// Otherwise, returns <see langword="default"/> of <typeparamref name="T"/>. | |
/// </summary> | |
T? UnwrapOrDefault(); | |
IResult<U, E> Map<U>(Func<T, U> mapFn); | |
U MapOr<U>(U defaultOk, Func<T, U> mapFn); | |
/// <summary> | |
/// <see langword="true"/> if <see langword="this"/> is <see cref="Result{TOk, TErr}.Err"/> else <see langword="false"/> | |
/// </summary> | |
bool IsOk(); | |
/// <summary> | |
/// <see langword="true"/> if <see langword="this"/> is <see cref="Result{TOk, TErr}.Ok"/> else <see langword="false"/> | |
/// </summary> | |
public bool IsErr(); | |
} | |
/// <summary> | |
/// The exception that is thrown when <see cref="Result{TOk, TErr}.Unwrap"/> is called on an instance that wraps an error instead of valid data. | |
/// </summary> | |
/// <param name="InnerException">The <typeparamref name="E"/> that was actually contained by the <see cref="Result{TOk, TErr}"/> instance.</param> | |
public class UnwrapError<E>(E err) : Exception($"Tried to unwrap an error type '{typeof(E).Name}': {err}") { | |
public E Error { get; } = err; | |
} | |
/// <summary> | |
/// An object that contains either <typeparamref name="T"/> or <typeparamref name="E"/>, depending on the outcome of a method call. | |
/// </summary> | |
/// <typeparam name="T"/> | |
/// <typeparam name="E"/> | |
public abstract class Result<T, E> : IResult<T, E> { | |
private Result() { } | |
public sealed class Ok(T value) : Result<T, E> { | |
public T Value { get; } = value; | |
public void Deconstruct(out T value) => value = Value; | |
} | |
public sealed class Err(E value) : Result<T, E> { | |
public E Value { get; } = value; | |
public void Deconstruct(out E value) => value = Value; | |
} | |
public static implicit operator Result<T, E>(T ok) => new Ok(ok); | |
public static implicit operator Result<T, E>(E err) => new Err(err); | |
public static implicit operator Result<T, E>(LateBoundOk<T> ok) => new Ok(ok.Ok); | |
public static implicit operator Result<T, E>(LateBoundErr<E> err) => new Err(err.Err); | |
/// <summary> | |
/// Performs an infallible transformation to an instance of <see cref="Result{T, E}.Ok"/> to produce a <see cref="Result{U, E}"/>.<br/> | |
/// Maps a <see cref="Result{T, E}"/> to <see cref="Result{U, E}"/> using <paramref name="mapFn"/> if it contains an <see cref="Ok"/> value. | |
/// Otherwise, it forwards <see cref="Err"/> without calling <paramref name="mapFn"/>. | |
/// </summary> | |
/// <typeparam name="U">The type returned from <paramref name="mapFn"/></typeparam> | |
/// <param name="mapFn">Converts from <typeparamref name="T"/> to <typeparamref name="U"/></param> | |
/// <returns></returns> | |
/// <exception cref="UnreachableException"></exception> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Result<U, E> Map<U>(Func<T, U> mapFn) => this switch { | |
Ok(T ok) => mapFn(ok), | |
Err(E err) => err, | |
_ => throw new UnreachableException() | |
}; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
IResult<U, E> IResult<T, E>.Map<U>(Func<T, U> mapFn) => Map(mapFn); | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public U MapOr<U>(U defaultValue, Func<T, U> mapFn) => this switch { | |
Ok(T ok) => mapFn(ok), | |
Err(E err) => defaultValue, | |
_ => throw new UnreachableException() | |
}; | |
/// <summary> | |
/// Enables the caller to progressively and conditionally chain fallible operations together. | |
/// Maps the value contained <see cref="Result{T, E}"/> to <see cref="Result{U, E}"/> using <paramref name="mapFn"/> if it contains an <see cref="Ok"/> value. | |
/// Otherwise, it forwards <see cref="Err"/> without calling <paramref name="mapFn"/>.<br/> | |
/// </summary> | |
/// <typeparam name="U"></typeparam> | |
/// <param name="mapFn"></param> | |
/// <returns></returns> | |
/// <exception cref="UnreachableException"></exception> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Result<U, E> AndThen<U>(Func<T, Result<U, E>> mapFn) => this switch { | |
Ok(T ok) => mapFn(ok), | |
Err(E err) => err, | |
_ => throw new UnreachableException() | |
}; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Result<T, E> AndThen(Func<T, Result<T, E>> mapFn) => this switch { | |
Ok(T ok) => mapFn(ok), | |
Err(E err) => err, | |
_ => throw new UnreachableException() | |
}; | |
/// <summary> | |
/// Enables the caller to progressively and conditionally chain fallible operations together. | |
/// Maps the value contained <see cref="Result{T, E}"/> to <see cref="Result{T, F}"/> using <paramref name="mapFn"/> if it contains an <see cref="Err"/> value. | |
/// Otherwise, it forwards <see cref="Ok"/> without calling <paramref name="mapFn"/>.<br/> | |
/// </summary> | |
/// <typeparam name="U"></typeparam> | |
/// <param name="mapFn"></param> | |
/// <returns></returns> | |
/// <exception cref="UnreachableException"></exception> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Result<T, F> OrElse<F>(Func<E, Result<T, F>> mapFn) => this switch { | |
Ok(T ok) => ok, | |
Err(E err) => mapFn(err), | |
_ => throw new UnreachableException() | |
}; | |
/// <summary> | |
/// Performs an infallible transformation to an instance of <see cref="Err"/> to produce a <see cref="Result{T, F}"/>.<br/> | |
/// Maps a <see cref="Result{T, E}"/> to <see cref="Result{T, F}"/> using <paramref name="mapFn"/> if it contains an <see cref="Err"/> value. | |
/// Otherwise, it forwards <see cref="Ok"/> without calling <paramref name="mapFn"/>. | |
/// </summary> | |
/// <typeparam name="U">The type returned from <paramref name="mapFn"/></typeparam> | |
/// <param name="mapFn">Converts from <typeparamref name="T"/> to <typeparamref name="U"/></param> | |
/// <returns></returns> | |
/// <exception cref="UnreachableException"></exception> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Result<T, F> MapErr<F>(Func<E, F> mapFn) => this switch { | |
Ok(T ok) => ok, | |
Err(E err) => mapFn(err), | |
_ => throw new UnreachableException() | |
}; | |
/// <inheritdoc/> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public T Unwrap() => this switch { | |
Ok(T value) => value, | |
Err(E err) => throw new UnwrapError<E>(err), | |
_ => throw new UnreachableException() | |
}; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public T UnwrapOr(T defaultValue) => this switch { | |
Ok(T value) => value, | |
Err => defaultValue, | |
_ => throw new UnreachableException() | |
}; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public E UnwrapErrOr(E defaultValue) => this switch { | |
Err(E value) => value, | |
Ok => defaultValue, | |
_ => throw new UnreachableException() | |
}; | |
/// <inheritdoc/> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public E UnwrapErr() => this switch { | |
Ok(T value) => throw new UnwrapError<T>(value), | |
Err(E err) => err, | |
_ => throw new UnreachableException() | |
}; | |
/// <inheritdoc/> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public T? UnwrapOrDefault() => this switch { | |
Ok(T value) => value, | |
Err => default, | |
_ => throw new UnreachableException() | |
}; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public T UnwrapOrElse(Func<T> okFn) => this switch { | |
Ok(T ok) => ok, | |
_ => okFn() | |
}; | |
/// <inheritdoc/> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public bool IsOk() => this is Ok; | |
/// <inheritdoc/> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public bool IsErr() => this is Err; | |
} | |
public record LateBoundOk<T>(T Ok); | |
public record LateBoundErr<E>(E Err); | |
public static class Result { | |
public static LateBoundOk<T> Ok<T>(T ok) => new(ok); | |
public static LateBoundErr<E> Err<E>(E err) => new(err); | |
} | |
public static class EnumerableExtensions { | |
public static Result<List<T>, E> Collect<T, E>(this IEnumerable<Result<T, E>> results) { | |
List<T> items = []; | |
var enumerator = results.GetEnumerator(); | |
while (enumerator.MoveNext()) { | |
var cur = enumerator.Current; | |
if (cur.IsOk()) { | |
items.Add(cur.Unwrap()); | |
} else { | |
return cur.UnwrapErr(); | |
} | |
} | |
return items; | |
} | |
public static Result<T?, E> Transpose<T, E>(this Result<T, E>? result) => result switch { | |
null => new Result<T?, E>.Ok(default), | |
{ } ok => ok as Result<T?, E> | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment