Skip to content

Instantly share code, notes, and snippets.

@arturaz
Last active April 19, 2023 12:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save arturaz/2af8261fd2efeb56c8ba2fbaf71ac383 to your computer and use it in GitHub Desktop.
Save arturaz/2af8261fd2efeb56c8ba2fbaf71ac383 to your computer and use it in GitHub Desktop.
Stack safe implementation of an IO monad in C#
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._;
});
}
}
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