Skip to content

Instantly share code, notes, and snippets.

@cartercanedy
Last active March 7, 2024 22:26
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> type
using System;
// makes sure that all references passed to this api are valid
// imho calls yielding nulls should actually yield err/exception if using this interface
#nullable enable
namespace Result;
/// <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.
/// </summary>
/// <typeparam name="TOk">The type returned by a successful call.</typeparam>
/// <typeparam name="TErr">The exception type that is returned on a failed call.</typeparam>
public interface IResult<out TOk, out TErr>
where TErr : Exception
{
interface IOk : IResult<TOk, TErr> { TOk ok { get; } }
interface IErr : IResult<TOk, TErr> { TErr err { get; } }
/// <summary>
/// Returns <typeparamref name="TOk"/> if bound to valid data.<br/>
/// Otherwise, throws <see cref="UnwrapError{TErr}"/> with the <typeparamref name="TErr"/> instance that was contained.
/// </summary>
/// <exception cref="UnwrapError{TErr}"/>
TOk Unwrap();
/// <summary>
/// Returns <typeparamref name="TOk"/> if bound to valid data.<br/>
/// Otherwise, returns <see langword="default"/> of <typeparamref name="TOk"/>.
/// </summary>
TOk? UnwrapOrDefault();
/// <summary>
/// <c><see langword="true"/> if <see langword="this"/> is <see cref="Result{TOk, TErr}.Err"/> else <see langword="false"/></c>
/// </summary>
bool IsOk();
/// <summary>
/// <c><see langword="true"/> if <see langword="this"/> is <see cref="Result{TOk, TErr}.Ok"/> else <see langword="false"/></c>
/// </summary>
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="TErr"/> that was actually contained by the <see cref="Result{TOk, TErr}"/> instance.</param>
public class UnwrapError<TErr>(TErr innerException)
: Exception("Tried to unwrap an error", innerException)
where TErr : Exception
{
public TErr Exception { get; } = innerException;
}
/// <summary>
/// An object that contains either <typeparamref name="TOk"/> or <typeparamref name="TErr"/>, depending on the outcome of a method call.
/// </summary>
/// <typeparam name="TOk"/>
/// <typeparam name="TErr"/>
public record Result<TOk, TErr> : IResult<TOk, TErr>
where TErr : Exception
{
public record Ok(TOk ok) : Result<TOk, TErr>, IResult<TOk, TErr>.IOk { }
public record Err(TErr err) : Result<TOk, TErr>, IResult<TOk, TErr>.IErr { }
public static implicit operator Result<TOk, TErr>(TOk ok) => new Ok(ok);
public static implicit operator Result<TOk, TErr>(TErr err)
{
if (err.StackTrace is null)
try { throw err; } catch { } // not pretty, but should preserve the stack trace if converted from an unthrown exception instance
return new Err(err);
}
/// <inheritdoc/>
public TOk Unwrap() =>
this switch
{
Ok ok => ok.ok,
Err err => throw new UnwrapError<TErr>(err.err),
_ => throw new Exception("never")
};
/// <inheritdoc/>
public TOk? UnwrapOrDefault() =>
this switch
{
Ok ok => ok.ok,
_ => default
};
/// <inheritdoc/>
public bool IsErr() => this is Err;
/// <inheritdoc/>
public bool IsOk() => this is Ok;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment