Last active
April 19, 2023 12:48
-
-
Save arturaz/2af8261fd2efeb56c8ba2fbaf71ac383 to your computer and use it in GitHub Desktop.
Stack safe implementation of an IO monad in C#
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
using System; | |
using JetBrains.Annotations; | |
using pzd.lib.data; | |
using pzd.lib.exts; | |
namespace pzd.lib.functional { | |
/// <summary> | |
/// Allows encapsulating side effects and composing them. | |
/// </summary> | |
// ReSharper disable once UnusedTypeParameter | |
[PublicAPI] public interface IO<out A> { | |
/// <summary> | |
/// Execute the IO returning a <see cref="TailRec.IResult{A}"/>. You probably want to use | |
/// <see cref="IO.run{A}"/> instead. | |
/// </summary> | |
TailRec.IResult<A> runTailRecResult(); | |
} | |
[PublicAPI] public static class IO { | |
#region Implementations | |
/// <summary>Simply returns a know value.</summary> | |
sealed class Return<A> : IO<A> { | |
public readonly A value; | |
public Return(A value) => this.value = value; | |
public TailRec.IResult<A> runTailRecResult() => TailRec.pure(value); | |
} | |
/// <summary>Stores a suspended computation that produces a value.</summary> | |
sealed class Suspend<A> : IO<A> { | |
public readonly Func<A> resume; | |
public Suspend(Func<A> resume) => this.resume = resume; | |
public TailRec.IResult<A> runTailRecResult() => TailRec.pure(resume()); | |
} | |
sealed class FlatMap<A, B> : IFlatMap<B> { | |
/// <summary><see cref="IO{A}"/> that we have to run first.</summary> | |
readonly IO<A> subroutine; | |
/// <summary>Function that provides <see cref="IO{A}"/> from <see cref="B"/>.</summary> | |
readonly Func<A, IO<B>> aToIoOfB; | |
public FlatMap(IO<A> subroutine, Func<A, IO<B>> aToIoOfB) { | |
this.subroutine = subroutine; | |
this.aToIoOfB = aToIoOfB; | |
} | |
public TailRec.IResult<B> runTailRecResult() { | |
var io = subroutine switch { | |
Return<A> return_ => aToIoOfB(return_.value), | |
Suspend<A> suspend => aToIoOfB(suspend.resume()), | |
IFlatMap<A> flatMap => flatMap.reassociate(aToIoOfB), | |
_ => throw new ArgumentOutOfRangeException(nameof(subroutine), subroutine, "unknown value") | |
}; | |
return TailRec.next(() => io.runTailRecResult()); | |
} | |
IO<X> IFlatMap<B>.reassociate<X>(Func<B, IO<X>> bToIoOfX) => | |
new FlatMap<A, X>( | |
subroutine, | |
a => { | |
var bIo = aToIoOfB(a); | |
return new FlatMap<B, X>(bIo, bToIoOfX); | |
} | |
); | |
} | |
/// <summary> | |
/// Interface for the FlatMap case that only exposes <see cref="B"/>. | |
/// | |
/// Used in <see cref="FlatMap{A,B}.runTailRecResult"/> | |
/// </summary> | |
interface IFlatMap<out B> : IO<B> { | |
IO<X> reassociate<X>(Func<B, IO<X>> aToIoOfX); | |
} | |
#endregion | |
/// <summary>Execute the IO computing the result in a stack safe way.</summary> | |
public static A run<A>(this IO<A> io) => TailRec.run(io.runTailRecResult); | |
public static IO<B> map<A, B>(this IO<A> aIo, Func<A, B> mapper) => | |
new FlatMap<A, B>(aIo, a => new Return<B>(mapper(a))); | |
public static IO<B> flatMap<A, B>(this IO<A> aIo, Func<A, IO<B>> mapper) => | |
new FlatMap<A, B>(aIo, mapper); | |
public static IO<B1> flatMap<A, B, B1>(this IO<A> aIo, Func<A, IO<B>> mapper, Func<A, B, B1> g) => | |
new FlatMap<A, B1>(aIo, a => mapper(a).map(b => g(a, b))); | |
public static readonly IO<Unit> empty = a(Unit._); | |
public static IO<A> a<A>(A a) => new Return<A>(a); | |
public static IO<A> a<A>(Func<A> fn) => new Suspend<A>(fn); | |
public static IO<Unit> a(Action action) => a(() => { | |
action(); | |
return Unit._; | |
}); | |
} | |
} |
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
using System; | |
namespace pzd.lib.data { | |
/// <summary> | |
/// Allows you to write tail-recursive code in C#. | |
/// | |
/// Code from https://thomaslevesque.com/2011/09/02/tail-recursion-in-c/ | |
/// </summary> | |
public static class TailRec { | |
public static T run<T>(Func<IResult<T>> func) { | |
do { | |
var recursionResult = func(); | |
if (recursionResult.isFinalResult) return recursionResult.result; | |
func = recursionResult.nextStep; | |
} while (true); | |
} | |
public static IResult<T> pure<T>(T result) => | |
new Result<T>(true, result, null); | |
public static IResult<T> next<T>(Func<IResult<T>> nextStep) => | |
new Result<T>(false, default, nextStep); | |
public interface IResult<out A> { | |
bool isFinalResult { get; } | |
A result { get; } | |
Func<IResult<A>> nextStep { get; } | |
} | |
class Result<T> : IResult<T> { | |
public bool isFinalResult { get; } | |
public T result { get; } | |
public Func<IResult<T>> nextStep { get; } | |
internal Result(bool isFinalResult, T result, Func<IResult<T>> nextStep) { | |
this.isFinalResult = isFinalResult; | |
this.result = result; | |
this.nextStep = nextStep; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment