No more Readers in my code!
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.