Algebraic Effect Monad to separate pure and impure code
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&lt;TIn,TOut&gt;). 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&lt;'E,'a&gt; =
     private EffReq of ('E -&gt; 'a)

let map (f : 'a -&gt; 'b) (EffReq p : Eff&lt;'E,'a&gt;) : Eff&lt;'E,'b&gt; =
    EffReq (f &lt;&lt; p)

// Argh F# blocks us from using unit, pure, or return...!
let init (x : 'a) : Eff&lt;'E,'a&gt; =
    EffReq (fun _ -&gt; x)

let run (io : 'E) (EffReq p : Eff&lt;'E,'a&gt;) : 'a =
    p io

let join (e : Eff&lt;'E,Eff&lt;'E,'a&gt;&gt;) : Eff&lt;'E,'a&gt; =
    EffReq (fun performEff -&gt; run performEff e |&gt; run performEff)

let bind (f : 'a -&gt; Eff&lt;'E,'b&gt;) (e : Eff&lt;'E,'a&gt;) : Eff&lt;'E,'b&gt; =
    join &lt;| map f e

let request = EffReq


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

module FileIO

open Effect

type FileIO = {
    ReadAllText : string -&gt; string
}

type IOEff&lt;'a&gt; = Eff&lt;FileIO,'a&gt;

let readAllText (path : string) : IOEff&lt;string&gt; =
    Effect.request (fun io -&gt; 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).