Skip to content

Instantly share code, notes, and snippets.

@louthy
Last active April 2, 2022 16:20
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save louthy/524fbe8965d3a2aae1b576cdd8e971e4 to your computer and use it in GitHub Desktop.
Save louthy/524fbe8965d3a2aae1b576cdd8e971e4 to your computer and use it in GitHub Desktop.
C# Free Monad
//
// See https://github.com/louthy/language-ext
//
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using LanguageExt;
using static LanguageExt.Prelude;
using static LanguageExt.List;
namespace ConsoleApp8
{
public static class TestIO
{
static IO<Unit> TotallyPureIOComputation(string path) =>
from lines in IO.ReadAllLines(path)
from _1 in IO.Log($"There are {lines.Count} lines")
from _2 in IO.Log($"Prepending line numbers")
let newLines = zip(Range(1, Int32.MaxValue), lines)
.Map((i, line) => $"{i} {line}")
.ToSeq()
from _3 in IO.WriteAllLines(path, newLines)
from _4 in IO.Log($"Lines prepended and file saved successfully")
select unit;
public static void Test(string path)
{
var computation = TotallyPureIOComputation(path);
var mockedResult = MockInterpreter.Interpet(computation);
var liveResult = LiveInterpreter.Interpet(computation);
var liveAsyncResult = LiveInterpreterAsync.Interpet(computation).Result;
}
}
public static class LiveInterpreter
{
public static A Interpet<A>(IO<A> ma) =>
ma is IO<A>.Return r ? r.Value
: ma is IO<A>.Faulted e ? throw new Exception(e.Message)
: ma is IO<A>.ReadAllLines ra ? Interpet(ra.Next(File.ReadAllLines(ra.Path).ToSeq()))
: ma is IO<A>.WriteAllLines wa ? Interpet(wa.Next(WriteAllLines(wa.Path, wa.Output)))
: ma is IO<A>.Log log ? Interpet(log.Next(Log(log.Output)))
: throw new NotSupportedException();
static Unit Log(string output)
{
Console.WriteLine(output);
return unit;
}
static Unit WriteAllLines(string path, Seq<string> output)
{
File.WriteAllLines(path, output.ToArray());
return unit;
}
}
public static class LiveInterpreterAsync
{
public static async Task<A> Interpet<A>(IO<A> ma) =>
ma is IO<A>.Return r ? r.Value
: ma is IO<A>.Faulted e ? throw new Exception(e.Message)
: ma is IO<A>.ReadAllLines ra ? await Interpet(ra.Next(await ReadAllLines(ra.Path)))
: ma is IO<A>.WriteAllLines wa ? await Interpet(wa.Next(await WriteAllLines(wa.Path, wa.Output)))
: ma is IO<A>.Log log ? await Interpet(log.Next(Log(log.Output)))
: throw new NotSupportedException();
static Unit Log(string output)
{
Console.WriteLine(output);
return unit;
}
static Task<Unit> WriteAllLines(string path, Seq<string> output) => Task.Run(() =>
{
File.WriteAllLines(path, output.ToArray());
return unit;
});
static Task<Seq<string>> ReadAllLines(string path) =>
Task.Run(() => File.ReadAllLines(path).ToSeq());
}
public static class MockInterpreter
{
public static A Interpet<A>(IO<A> ma) =>
ma is IO<A>.Return r ? r.Value
: ma is IO<A>.Faulted e ? throw new Exception(e.Message)
: ma is IO<A>.ReadAllLines ra ? Interpet(ra.Next(MockReadAllLines(ra.Path)))
: ma is IO<A>.WriteAllLines wa ? Interpet(wa.Next(WriteAllLines(wa.Path, wa.Output)))
: ma is IO<A>.Log log ? Interpet(log.Next(Log(log.Output)))
: throw new NotSupportedException();
static Seq<string> MockReadAllLines(string path) =>
Seq("Hello", "World");
static Unit Log(string output)
{
Console.WriteLine(output);
return unit;
}
static Unit WriteAllLines(string path, Seq<string> output)
{
return unit;
}
}
public static class IO
{
public static IO<A> Return<A>(A value) => new IO<A>.Return(value);
public static IO<Unit> Faulted(string error) => new IO<Unit>.Faulted(error, Return);
public static IO<Seq<string>> ReadAllLines(string path) => new IO<Seq<string>>.ReadAllLines(path, Return);
public static IO<Unit> WriteAllLines(string path, Seq<string> output) => new IO<Unit>.WriteAllLines(path, output, Return);
public static IO<Unit> Log(string output) => new IO<Unit>.Log(output, Return);
}
public static class IOExt
{
public static IO<B> Bind<A, B>(this IO<A> ma, Func<A, IO<B>> f) =>
ma is IO<A>.Return r ? f(r.Value)
: ma is IO<A>.Faulted e ? new IO<B>.Faulted(e.Message, n => e.Next(n).Bind(f))
: ma is IO<A>.ReadAllLines ra ? new IO<B>.ReadAllLines(ra.Path, n => ra.Next(n).Bind(f))
: ma is IO<A>.WriteAllLines wa ? new IO<B>.WriteAllLines(wa.Path, wa.Output, n => wa.Next(n).Bind(f))
: ma is IO<A>.Log log ? new IO<B>.Log(log.Output, n => log.Next(n).Bind(f)) as IO<B>
: throw new NotSupportedException();
public static IO<B> Select<A, B>(this IO<A> ma, Func<A, B> f) =>
ma.Bind(a => IO.Return(f(a)));
public static IO<C> SelectMany<A, B, C>(this IO<A> ma, Func<A, IO<B>> bind, Func<A, B, C> project) =>
ma.Bind(a => bind(a).Select(b => project(a, b)));
}
public abstract class IO<A>
{
public class Return : IO<A>
{
public readonly A Value;
public Return(A value) =>
Value = value;
}
public class Faulted : IO<A>
{
public readonly string Message;
public readonly Func<Unit, IO<A>> Next;
public Faulted(string message, Func<Unit, IO<A>> next)
{
Message = message;
Next = next;
}
}
public class ReadAllLines : IO<A>
{
public readonly string Path;
public readonly Func<Seq<string>, IO<A>> Next;
public ReadAllLines(string path, Func<Seq<string>, IO<A>> next)
{
Path = path;
Next = next;
}
}
public class WriteAllLines : IO<A>
{
public readonly string Path;
public readonly Seq<string> Output;
public readonly Func<Unit, IO<A>> Next;
public WriteAllLines(string path, Seq<string> output, Func<Unit, IO<A>> next)
{
Path = path;
Output = output;
Next = next;
}
}
public class Log : IO<A>
{
public readonly string Output;
public readonly Func<Unit, IO<A>> Next;
public Log(string output, Func<Unit, IO<A>> next)
{
Output = output;
Next = next;
}
}
}
}
@dadhi
Copy link

dadhi commented Nov 9, 2017

When creating ADT cases the constructors may be even shortened with ValueTuples ;)

public Log(string output, Func<Unit, IO<A>> next) => 
    (Output, Next) = (output, next);

Btw, thanks for the cool complete example!

@dadhi
Copy link

dadhi commented Nov 10, 2017

Also it would be interesting to see how bigger is the code without LanguageExt. I assume not so much. Then it will be a proof that no magic required for doing FP in C#.

@dadhi
Copy link

dadhi commented Nov 11, 2017

I did one without LanguageExt as proof: https://gist.github.com/dadhi/026350db603dd348af6c0369a9a5bd0b

@dadhi
Copy link

dadhi commented Jan 11, 2018

Hey, I wanted to understand in detail how the Free monad works and what it solves and so, ended up with a newer example with monadic boilerplate stripped off: https://gist.github.com/dadhi/59cfc698f6dc6e31b722cd804aae185a

@dzmitry-lahoda
Copy link

seems can use discards instead of _1

@dzmitry-lahoda
Copy link

normal C# with MoreLinq could do well zip(Range(1, Int32.MaxValue), lines) .Map((i, line) => $"{i} {line}") .ToSeq()

@yuretz
Copy link

yuretz commented Oct 16, 2021

Here is the same example implemented using an alternative approach to free monads in C#: https://github.com/yuretz/FreeAwait/blob/master/samples/TestIO/Main.cs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment