Reader Monad

Reader<TEnvironment, TResult> is a computation that needs an environment to run. It is a delegate, nothing more:

public delegate TResult Reader<in TEnvironment, out TResult>(TEnvironment environment);

A reader is a function from the environment to a result. What makes it a monad is that readers compose: you can build a bigger reader out of smaller ones with Select and SelectMany, and every one of them receives the same environment when the whole thing is finally run. The environment is typically a configuration, a set of services, or a connection, and the reader is the functional alternative to passing that object through every method signature, or to a dependency injection container.

Creating readers

Reader<TEnvironment, TResult> Reader<TEnvironment>.Return<TResult>(TResult value)       // ignores the environment
Reader<TEnvironment, TResult> Reader<TEnvironment>.FromFunc<TResult>(Func<TEnvironment, TResult> function)
Reader<TEnvironment, Unit> Reader<TEnvironment>.FromAction(Action<TEnvironment> action)

Because Reader is a delegate type, any lambda or method with the right shape converts to it, so most readers are written directly:

record Configuration(char QuoteChar);

static Reader<Configuration, string> Quote(string text)
    => config => $"{config.QuoteChar}{text}{config.QuoteChar}";

Reader<TEnvironment>.Return lifts a plain value into a reader that does not look at the environment. The split into a generic class with the environment and a generic method with the result exists so that Return can be passed as a method group where the result type is inferred.

Composing readers

Select transforms the result; SelectMany chains a reader with a function producing the next reader. Query syntax works, and the environment is threaded through invisibly:

static Reader<Configuration, string> QuoteAll(string start, string middle, string end)
    => from s in Quote(start)
       from m in Quote(middle)
       from e in Quote(end)
       select $"{s}{m}{e}";

Nothing has run yet. QuoteAll returns a reader, and the environment is supplied once, at the edge:

var reader = QuoteAll("a", "b", "c");

reader(new Configuration('"'));   // "\"a\"\"b\"\"c\""
reader(new Configuration('*'));   // "*a**b**c*"

The same reader run with two configurations gives two results. That is the point: the code that builds the computation does not know or care which environment it will see.

Readers over sequences

Sequence and Traverse on IEnumerable<T> work with readers: a sequence of readers becomes a reader of a sequence, run lazily under one environment.

static Reader<Configuration, IEnumerable<string>> QuoteEach(IEnumerable<string> texts)
    => texts.Traverse(Quote);

The other monads have Traverse and Sequence overloads for readers as well, for example Option<Reader<E, T>> to Reader<E, Option<T>>.

Side effects

FromAction wraps an Action<TEnvironment> into a Reader<TEnvironment, Unit>, so an effectful step can take part in a query. The action runs when the reader is run, not when it is created.

static Reader<Configuration, Unit> Log(string message)
    => Reader<Configuration>.FromAction(config => Console.WriteLine($"[{config.QuoteChar}] {message}"));