Skip to content

Instantly share code, notes, and snippets.

@cartercanedy
Last active March 27, 2025 23:21
Show Gist options
  • Save cartercanedy/b4c20200e79eb693caefdcc6791633d3 to your computer and use it in GitHub Desktop.
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 :)
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