Reader Monad Explored

- 3 mins

The Reader Monad (also referred to as the Environment monad) is commonly used to thread configuration through application code to call sites that depend on it. It offers a form of dependency injection.

Ex. Scala

// DAO.scala
trait DAO {
    type Id = String
    def get(id: Id): Reader[Config, Future[Maybe[Model]]] = ???
}

object DAO extends DAO

// Controller.scala
object Controller {
    def userRoutes(env: Env) =
        get {
            path(Segment) { username =>
                pathEnd {
                    DAO.get(username).run(env.config)
                }
            }
        } ~ ...
}

Ex. JavaScript via the excellent lib monet

// dao.js
import { Maybe, Reader, } from 'monet'

export const get = Reader(config => ...)

// route.js
import { get, } from './dao'

export default env => {
    return {
        get(req, res, next){
            const promiseMaybeModel = get(req.params.id).run(env.config)
        }
    }
}

You’ve probably noticed that in both examples we’re using the Reader monad for threading state below the top-level entry point to our application. At the top level, we rely upon ordinary functions for passing dependencies.

At this point you might be wondering why we bother to use the Reader instead of a simple function. That seems to work well enough for the applications top-level, so why not for the remainder of the application?

Well, Reader is not without its problems.

Downsides:

Each function of a module (.e.g, DAO) replicates the Reader monad signature. This is fine for small modules, but becomes tiresome and error prone as the module size increases. Refactoring becomes a pain if the Reader signature changes as well.

export default {
    get: Reader(env => {}),
    update: Reader(env => {}),
}

The advantage of being able to write new functions in terms of existing functions that return a Reader turns out to not come up that often in my experience. I have used it a few times, but I’m not sure that I gained a ton from its use.

export const get = Reader(env => model)

// returns `Reader<Env, Id>`
export const getId = get.map(model => model.id)

export const getEmail = get.map(model => model.email)

The well-known awkwardness of dealing with stacked monads (in the absence of Monad Transformers) receives an additional monad on the stack. It would not be unusual for a DAO module to have a function with the signature id => Reader[Env, Future[Either[Error, Maybe[Model]]]]. This is unwieldy.

In JavaScript, the Promise/A+ specification does not conform to the monad contract, so one has to reach for a library with a conformant implementation.

After using this pattern in several applications, I’ve decided that while it’s a nice tool to have in your belt, other methods for dependency injection are better suited to most use cases.

I’ve largely moved to using functions and constructors for injecting dependencies in JavaScript for medium to large applications.

Crisson Jno-Charles

Crisson Jno-Charles

Fullstack Software Engineer specializing in Scala, JavaScript, and Java development

comments powered by Disqus
rss facebook twitter github youtube mail spotify instagram linkedin google pinterest medium vimeo