Skip to content

Instantly share code, notes, and snippets.

@fakhrulhilal
Last active September 28, 2024 08:39
Show Gist options
  • Save fakhrulhilal/ce1639fa967da591c7e3a2b13a7850e2 to your computer and use it in GitHub Desktop.
Save fakhrulhilal/ce1639fa967da591c7e3a2b13a7850e2 to your computer and use it in GitHub Desktop.
C# functional style for wrapping result without using actual exception
internal readonly struct Result<T>(bool successful, T result, Exception exception)
{
private bool Successful { get; } = successful;
public static implicit operator T(Result<T> wrapped) => wrapped.UnwrapOrThrow();
public static implicit operator Result<T>(T result) => new(true, result, default!);
public static implicit operator Result<T>(Exception error) => new(false, default!, error);
/// <summary>
/// Get backing value and convert it to another type
/// </summary>
/// <param name="success">A factory when successful</param>
/// <param name="error">A factory when not successful</param>
/// <typeparam name="TOutput">Output type</typeparam>
public TOutput Match<TOutput>(Func<T, TOutput> success, Func<Exception, TOutput> error) =>
Successful ? success(result) : error(exception);
/// <summary>
/// Convert backing value and wrap again for successful result
/// </summary>
/// <param name="success"></param>
/// <typeparam name="TOutput"></typeparam>
/// <returns>Wrapped result with different value</returns>
public Result<TOutput> Map<TOutput>(Func<T, TOutput> success) => Successful ? success(result) : exception;
/// <summary>
/// Get the actual value or throw when not successful
/// </summary>
/// <returns>The backing value</returns>
/// <exception cref="Exception">Exception thrown when not successful</exception>
public T UnwrapOrThrow() => Successful ? result : throw exception;
/// <summary>
/// Invoke an action when successful
/// </summary>
public async Task InvokeAsync(Func<T, Task> action) {
if (Successful) {
await action(result);
}
}
}
internal static class Functional
{
internal readonly struct Ctx<T1, T2>(T1 a, T2 b)
{
public T1 A { get; } = a;
public T2 B { get; } = b;
}
internal readonly struct Ctx<T1, T2, T3>(T1 a, T2 b, T3 c)
{
public T1 A { get; } = a;
public T2 B { get; } = b;
public T3 C { get; } = c;
}
/// <summary>
/// Expression wrapper. Used when expression is needed, but need some statements before returning response.
/// Useful when language becomes limitation when doing something, f.e. calculating result inside switch expression.
/// </summary>
/// <param name="factory"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T Build<T>(Func<T> factory) => factory();
public static async Task<Result<T>> ThrowWhenNull<T>(this Task<T?> fetch, string message) where T : class {
var result = await fetch;
return result is null ? new NullReferenceException(message) : result;
}
/// <summary>
/// Convert from input to an output when not <c>null</c>
/// </summary>
/// <param name="fetch">A factory to get input to be converted</param>
/// <param name="next">
/// Map previous <typeparamref name="T"/> to <typeparamref name="TOutput"/>
/// when <typeparamref name="T"/> is not <c>null</c>
/// </param>
/// <param name="notFoundMessage">Exception message when <c>null</c></param>
/// <returns>Wrapped result which can be an instance or exception when <c>null</c></returns>
public static async Task<Result<Ctx<T, TOutput>>> Then<T, TOutput>(this Task<T?> fetch,
Func<T, TOutput> next, string notFoundMessage) where T : class {
var result = await fetch;
return result is null ? new NullReferenceException(notFoundMessage) : new Ctx<T, TOutput>(result, next(result));
}
public static async Task<Result<Ctx<TInput, TOutput>>> Then<TInput, TOutput>(
this Task<Result<TInput>> fetch,
Func<TInput, TOutput> next) {
var outcome = await fetch;
return outcome.Map(input => new Ctx<TInput, TOutput>(input, next(input)));
}
/// <summary>
/// Convert from input to an output when not <c>null</c> and previous outcome is successful
/// </summary>
/// <param name="fetch">A factory to get previous outcome to be converted</param>
/// <param name="whenNotNull">A factory conversion when <typeparamref name="T2"/> is not null</param>
/// <param name="whenNull">A factory conversion when <typeparamref name="T2"/> is null</param>
/// <typeparam name="T1">Additional context for conversion</typeparam>
/// <typeparam name="T2">Input</typeparam>
/// <typeparam name="TOutput">Conversion output</typeparam>
/// <returns>Wrapped result which can be an instance or exception when <c>null</c></returns>
public static async Task<Result<Ctx<T1, T2?, TOutput>>> Then<T1, T2, TOutput>(
this Task<Result<Ctx<T1, T2?>>> fetch,
Func<T2, TOutput> whenNotNull, Func<TOutput> whenNull) where T2 : class {
var outcome = await fetch;
return outcome.Map(ctx =>
new Ctx<T1, T2?, TOutput>(ctx.A, ctx.B, ctx.B is null ? whenNull() : whenNotNull(ctx.B)));
}
/// <summary>
/// Convert from input to an output when not <c>null</c> and previous outcome is successful
/// </summary>
/// <param name="fetch">A factory to get previous outcome to be converted</param>
/// <param name="whenNotNull">A factory conversion when <typeparamref name="T2"/> is not null</param>
/// <param name="whenNull">A factory conversion when <typeparamref name="T2"/> is null</param>
/// <typeparam name="T1">Additional context for conversion</typeparam>
/// <typeparam name="T2">Input</typeparam>
/// <typeparam name="TOutput">Conversion output</typeparam>
/// <returns>Wrapped result which can be an instance or exception when <c>null</c></returns>
public static async Task<Result<Ctx<T1, T2?, TOutput>>> Then<T1, T2, TOutput>(
this Task<Result<Ctx<T1, T2?>>> fetch,
Func<T1, T2, TOutput> whenNotNull, Func<T1, TOutput> whenNull) where T2 : class {
var outcome = await fetch;
return outcome.Map(ctx =>
new Ctx<T1, T2?, TOutput>(ctx.A, ctx.B,
ctx.B is null ? whenNull(ctx.A) : whenNotNull(ctx.A, ctx.B)));
}
/// <summary>
/// Invoke an action when <typeparamref name="T3"/> is successful
/// </summary>
/// <param name="fetch">Previous outcome</param>
/// <param name="action">An asynchronous action to be invoked when successful</param>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="T3"></typeparam>
public static async Task<Result<Ctx<T1, T2?, T3>>> Then<T1, T2, T3>(
this Task<Result<Ctx<T1, T2?, T3>>> fetch,
Func<T1, T2, Task> action) {
var outcome = await fetch;
await outcome.InvokeAsync(async ctx =>
{
if (ctx.B is not null) {
await action(ctx.A, ctx.B);
}
});
return outcome;
}
/// <summary>
/// Unwrap the last result from outcome or throw exception when not successful
/// </summary>
/// <param name="outcome">Wrapped outcome</param>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="T3"></typeparam>
/// <returns><typeparamref name="T3"/> or throwing exception</returns>
/// <exception cref="Exception">An exception captured during previous execution</exception>
public static async Task<T3> UnwrapLastResult<T1, T2, T3>(this Task<Result<Ctx<T1, T2, T3>>> outcome) {
var result = await outcome;
return result.Match(ctx => ctx.C, exception => throw exception);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment