3. MTL IN 15 MINUTES - @PVILLEGA
• Pawel Szulc - A road trip with Monads:
https://www.youtube.com/watch?v=y_QHSDOV
• John De Goes - FP to the Max
https://www.youtube.com/watch?v=sxudIMiOo6
• Http4s error handling with Cats Meow MTL
https://typelevel.org/blog/2018/08/25/http4s-erro
4. MTL IN 15 MINUTES - @PVILLEGA
INTERPRETERS?
TAGLESS FINAL?
5. MTL IN 15 MINUTES - @PVILLEGA
final case class Topic(id: Int, title: String)
trait TopicServiceAlg[F[_]] {
def createTopic(id: Int,
title: String): F[Unit]
def getById(id: Int): F[Option[Topic]]
def delete(id: Int): F[Option[Topic]]
}
def aMethod[F[_]](
topicService: TopicServiceAlg[F]): F[Topic]
21. – Questions please :)
“If you work at UK’s home office or know
someone there, please talk to me after the
Q&A.”
Editor's Notes
Hi, welcome everyone. I’m Pere Villega, Scala contractor, and today I’m going to talk about MTL
Scala community seems to go over an annual cycle in which they choose a technique and spam it across conferences all year long. It happened with Free, Recursion schemes, and now with MTL. So we are going to talk about MTL, so we all understand what it is, and then we can forget about it next year :)
I need to start with some disclaimers. I recommend you to watch and read these presentations I link on the slide. This is a 15m talk, and MTL is something you can talk about for much longer. I will leave many things on the side, simplify others, and even make some mistakes. The aim of this talk is just to give you a small introduction on why does it exist and why is it useful, so then you can decide if you are interested on learning more about it.
Question for the audience: who knows about interpreters? And about tagless final? There was a talk by Dave Gurnell today that you should watch if you have not seen it, as it gives solid foundations for what we are going to discuss now.
From now on in the talk when I say tagless final please understand parameterized interfaces and interpreters. Let’s do a quick recap. This is a technique to separate our business domain from our implementation. Here we define an interface for our topic service, as a trait, without implementation, parameterised with an effect F. Something that we can use in our business logic, as we can see in this method. Note we are going to keep the code simple, so we focus on the relevant concepts
The advantage of doing that is that we can have multiple implementations of that trait that we can swap as needed. Ex java programmers will remember the rule of coding against the interface. We can have this test implementation backed up by a map. This removes the need of mocking frameworks.
Or our production implementation relying on an external service which uses IO to isolate the side effects.
We can use anything as F, including our own custom made F.
This defining our own F is where the problem starts. We want our F, our effect, to have some properties: error handling as we don’t throw exceptions. Logging. And passing configuration via a Reader, the FP way. And I’m skipping State and other properties we may want. So that is our F.
The problem is that when you try to use that, it doesn’t even work! Monads don’t compose. That is why we have monad transformers in first place. But which one is the correct stacking?
Also, you want monad transformers? I hope you like this for comprehension. Working with 3 layers of transformer stacked is a nightmare, unwrapping and wrapping monads each step. Please let me guide your attention to the fact this is a single row in the for comprehension.
And let’s not even start talking about performance, what all this unwrap-wrap and intermediate objects created do to the jvm.
So, we need a better way. We want to use the interfaces we defined, we like tagless final style, but we don’t want to make our lives hard with all those transformers.
Enter cats-mtl. cats-mtl provides transformer typeclasses for cats' Monads, Applicatives and Functors. The fact that it provides typeclasses is very very important, as typeclasses can be passed around via implicit parameters in our functions.
Cats mtl gives us several type classes for existing monad transformers in cats. Here we have some examples: ApplicativeAsk is the equivalent typeclass for ReaderT, to work with configuration. FunctorRaise, equivalent to EitherT. FunctorTell, to WriterT, for logging. MonadState, equivalent to StateT. And a few more. Note also that the names are descriptive of their capabilities, FunctorTell only requires F to be a Functor to work, following the principle of less power is better.
What does all this mean in practice? It means we can do this. We have a method that requires an implicit typeclass of type FunctorRaise, for our F.
Note how much we get in this definition:
We know our function can raise errors of type Error
We know F can be anything as long as it has an instance of the FunctorRaise typeclass we request
All this is checked at compile time, if the implicit doesn’t exist or there are conflicting implicits we will get a compile time error
We can test this function in isolation by swapping the implicit we use
Remember our stack before. Let’s implement equivalent type classes to that stack for a dev environment. Here we have the implementation for ApplicativeAsk, hardcoding some configuration. We have an F that must have an Applicative, and we return a hardcoded Config on Ask.
Or we can use this FunctorTell for printing to console when in development. Note that I cheated a bit by making F require applicative as I need ‘pure’.
With those definitions, our previous for comprehension can now become this, a much nicer code. There’s a lot to comment in here.
Note that we define our type depending on a single transformer: EitherT. There are ways to remove it, but requires using MonadError and IO, or other IO-equivalent libraries
We preserve all the functionality from the old stack via the implicits: configuration, error, logging. All validated at compile time
Note we define F as Monad here so we can have flatMap working for the for-comprehension
And, note that we have not defined our FunctorRaise anywhere. No, I didn’t forget it, this is not partial code. It’s there, in EitherT. Cats-mtl doesn’t just give us the typeclasses, it also provides default implementations for them, so if I have an EitherT in scope, I get my FunctorRaise, thank you very much.
We can test this by providing several implementations of our topic service, but we can also test things like the logging for example, we can provide a witness that stores all the logs instead of printing to console, and at the end of the test we verify they are as we expect.
Ok, so I hope you are convinced that cats-mtl is a good idea. That said, I’d recommend you to use it along this other library: meow-mtl. meow-mtl provides several improvements on top of cats-mtl.
- Easy composition of MTL-style functions (I’ll explain this later)
MTL instances for cats-effect compatible datatypes (same)
One is conflict-free implicits for sub-instances : in the previous code, remember we had to define F as Monad to be able to flatMap? If you have an effect that requires Monad, it derives that automatically for you. Which is neat.
Be careful with imports, though, read the readme accurately regarding which imports to use to avoid diverging implicits.
One selling point of meow was ‘Easy composition of MTL-style functions’. Let’s explain it using the example of the documentation of meow. Notice at the top the type AuthRequest, which has 2 elements, Headers and User. We also have 2 functions that depend on MonadState. One on MonadState of User, one on MonadState of Headers.
Our handleGreetRequest function depends on MonadState of AuthedRequest, and calls the previous functions. Now, usually this wouldn’t compile, as greetUser and addRequestIdHeader are missing the right implicit. But meow uses Optics and code generation, and it verifies that if we have an AutherRequest, we can get both a Header and a User, so it automatically provides the right implicits to the functions.
Another property was that meow provides MTL instances for cats-effect or monix compatible datatypes. This means you can use those types, which perform better than the monad transformer alternative. See the code in the slide, taken from the readme. Ref is an asynchronous, concurrent mutable reference. meow-mtl provides a MonadState instance for Ref.
In summary, we want MTL because we get all the benefits of a tagless final approach, without the nuisances of monad transformers, and without performance penalties.