HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Algebraic Effect Monad to separate pure and impure code

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
effectimpurepureseparatecodeandalgebraicmonad

Problem

I want to write my C# code in a way which makes effects (state, I/O, etc) explicit in the type signature. I have started to understand monads, and have some acquaintance with algebraic effects, though not deeply so. I have attempted to implement a monad for working with algebraic effects in C#. The purpose is to allow you to provide an impure function which is an interpreter over an algebraic representation of your effects at the time that you run the monad, but not before. In theory this allows you to write all of your code in a pure way, and even pass a pure function in for the effects for the sake of unit testing etc. The monad is intended to be generic with respect to what types you use to represent your effects, and it is built with a struct in order to not be nullable.

public struct Eff
{
    private readonly Func,TReturn> _wrappedParameter;

    private Eff(Func, TReturn> wrappedParameter)
    {
        _wrappedParameter = wrappedParameter;
    }

    public static Eff Return(TReturn obj)
    {
        return new Eff(_ => obj);
    }

    public static Eff Do(TIn input, Func continuation)
    {
        return new Eff(f => continuation(f(input)));
    }

    public TReturn Run(Func f)
    {
        return _wrappedParameter(f);
    }

    public Eff Map(Func g)
    {
        var wrappedParameter = _wrappedParameter;
        return new Eff(f => g(wrappedParameter(f)));
    }

    public Eff Bind(Func> continuation)
    {
        var wrappedParameter = _wrappedParameter;
        return new Eff(f => continuation(wrappedParameter(f)).Run(f));
    }
}


There are several things that could be brought up here (he concepts are so new with me that I have named things poorly; I haven't yet figured out how it would look to have multiple effect types playing together in your program; etc), but there's one thing in particular I am concerned about, which is that I am nervous that if an entire program were written inside this monad, it would have the potential to over

Solution

This is not a direct and full answer, but whereas I did not end up getting the opportunity I was hoping for to try this out on a C# project (yet), I did hit a perfect case for one in an F# project. I rewrote this monad in F#, implemented some concrete operations with it, and used it. I have it running in production with no problems so far, but it isn't using the constrained environment that I was describing in the question, so it hasn't given me full confidence in that yet.

After having done that, there are two main criticisms I have of the version above:

  • It's not very flexible. It really only allows you to have effects that conform to one type (Func). Moreover it really only allows you to have one effect of this type.



  • The three generic types are overkill and not necessary. The alternative will be demonstrated below.



What I came up with was this generic effect monad:

module Effect

type Eff =
private EffReq of ('E -> 'a)

let map (f : 'a -> 'b) (EffReq p : Eff) : Eff =
EffReq (f =
EffReq (fun _ -> x)

let run (io : 'E) (EffReq p : Eff) : 'a =
p io

let join (e : Eff>) : Eff =
EffReq (fun performEff -> run performEff e |> run performEff)

let bind (f : 'a -> Eff) (e : Eff) : Eff =
join

Specific operations can be implemented using
request, e.g.:

module FileIO

open Effect

type FileIO = {
ReadAllText : string -> string
}

type IOEff = Eff

let readAllText (path : string) : IOEff =
Effect.request (fun io -> io.ReadAllText path)
`

I know this is in a different language than the original question but the conceptual aspect is the most important part.

I don't consider this implementation fully battle-hardened but it should allow me to evolve the C# version in a good direction.

Note that this is really a Reader monad: it reads from an impure environment (and because that environment is impure, technically it is able to write to it as well).

Context

StackExchange Code Review Q#146499, answer score: 2

Revisions (0)

No revisions yet.