No more Readers in my code!

Alexander Zaidel
2 min readSep 19, 2018

--

Reader monad is a functional way of implementing dependency injection, but it’s not that popular. Let’s find out why. To do that we will solve a tiny task — find an author of the shortest tweet in twitter. It’s relatively easy:

  • Get tweets
  • Find the shortest one
  • Get user’s information by userId from the shortest tweet

Introducing a couple of traits:

and case classes:

The implementation for the traits would look like:

The last part is to wire everything together:

Overall it isn’t much different from a regular implementation, but ReaderT is everywhere and everything that is not ReaderT has to be lifted:

ReaderT.liftF(TwitterServiceStub.mostActiveUserId(tweets))ReaderT.liftF[IO, Environment, Option[User]](IO.pure(None))

Seems like the code is doomed by Readers. Could it be worse? Sure, more monad transformers:

Furthermore there is a performance penalty for using many monad transformers. And the most important is that we have already commited to ReaderT[IO, Environment, T]. To replace IO with Monix Task or Future we would have to change every trait and it’s implementation.

Final tagless and ApplicativeAsk to the rescue

Very recently I found a library called cats-mtl, which provides a typeclass ApplicativeAsk.

F[_] is a higher order type, most likely it will be a monad, like IO/Future.
E — environment of dependencies, in our example it’s Environment:

case class Environment(connectionPool: ConnectionPool, httpClient: HttpClient, config: Config)

Even though there are no restrictions on F, we have to provide and instance of Applicative[F], so we will be able to implement ask by lifting the E into F[E].

The last function is reader, we can think of it like a function that provides a specific (specific and lifted) dependency, out of environment E. For example, httpClient from Environment:

applicativeAsk.reader(env => env.httpClient)

Let’s implement the same shortest tweet task using final tagless over F[_]:

Time to think about F, what effects should it bring to the table?
It should be a Monad, because of sequential computation of the provided algebra and ApplicativeAsk for dependency injection.

Since our dependencies are fixed over Environment, we can introduce a type alias:

type EnvironmentAsk[F[_]] = ApplicativeAsk[F, Environment]

Thus F[_] will be constrained over EnvironmentAsk and Monad:

F[_] : EnvironmentAsk : Monad

The implementation for our final tagless algebra is pretty straightforward, except the way we summon an instance of EnvironmentAsk:

private val env = implicitly[EnvironmentAsk[F]]

Our shortest tweet author algebra:

And it’s time to decide what our F[_] will be. Assuming that F[_] is IO, ApplicativeAsk can be implemented this way:

A few final lines of code and we are done:

To summarize, ApplicativeAsk provides us ability to reason about dependencies without “commiting to monads” during implementation. It doesn’t spread around every method in code. We don’t need to use monad transformers.

--

--