Deferring commitments: Tagless Final

Calvin F
Calvin F
Jan 30, 2018 · 9 min read

When writing services in typed languages, we usually write an interface and an implementation for that interface. We refer to the implementation by its interface. In the services that we write, it’s common to see something like this:

A Repository interface

Notice that all those operations return a Future indicating that these are asynchronous operations. In Functional Programming, we explicitly encode effects like asynchronicity (Future), optionality (Option), failure (Try or Either) and many other different types of effects. This makes it clear to the developers that they are dealing with some kind of effect and they need to be careful. For example, In JavaScript and Java, it’s common to use null to represent values that might not be there but the problem is that this is not explicit and if you forget to do your null checks, you will end up with null pointer exceptions. Using Options to indicate this explicitly makes it impossible to bypass this check and makes for safer APIs.

There is yet another level of abstraction we can apply to this interface. This would be abstracting over the effect (Future) in the above example. Before we do that, I would like to show you some sample usage of this repository from a service

A service that makes use of the Forecast Repository interface

Notice here, we make use of the Future’s flatMap and map operation (explicitly and implicitly through for comprehensions) in order to sequence multiple asynchronous operations together. For example, for addForecastByLatLong, we first obtain the current weather at a particular latitude and longitude (which is an asynchronous operation) and use the result to run another asynchronous computation to populate the repository.

So remember how we were talking about abstracting over the effect in the repository. If we do this, we still need to be able to execute these flatMaps and maps in order to sequence effectful actions so the service can do its job. We’ll see how to do this in just a minute. Let’s create an Order Repository where we abstract over that effect.

Instead of using the Future effect, we use an abstract F effect

F is a higher kinded type/function at the type level/type level constructor. Examples are Future, Option, List, etc. They are not proper types but if you provide them proper types as arguments, the result will be a proper type. Since that statement was a bit confusing, let’s look at an example. List is a type constructor and Int is a proper type. If I apply Int to List then I get List[Int] which is a proper type. So far, all we have done is used a generic higher kinded type (F) instead of a concrete one (Future).

Now, let’s say we have a service that wants to use this Order Repository, we would end up hitting a wall!

How do you flatMap and map on F?

We don’t have the ability to treat F like a Future which has Future.successful, flatMap and map. At this point, you could argue and say, well Order Service could ask for a OrderRepository[Future] and call it a day. This would definitely work but you want to be able to write your functions without being tied to an implementation as much as possible. Doing this gives you the ability to swap out just the repository implementations without changing the service and that’s pretty powerful.

Using typeclasses, we can make demands of the F effect and effectively constrain it to types that have the ability to lift proper types into the F context (like Future.successful), be able to map and flatMap.

A brief introduction to typeclasses

The syntax is a bit clunky but with the use of Scala’s implicits, we can create a much slicker API.

Use implicits to automatically inject Addable instances

This is an example of a typeclass method. Notice that in order to use the add method, your A’s need to have a typeclass implementation of Addable[A]. So for example, if we wanted add to add our two Orders then we must have an Addable[Order] implementation. The implicit keyword is used to pull that Addable[Order] implementation in automatically without you having to provide it explicitly. So in order to use this, we can do a little something like this

Make addableOrder an implicit value primed for automatic injection where applicable

There’s a more succinct syntax called the Context Bound that can be used for the add function. That looks like this:

Using the context bound instead of an implicit parameter

If you don’t plan to use the typeclass directly and want to pass it through, this is a good option.

Now that you have an idea of how to make demands to types that want to use your functions by means of typeclass constraints we are one step closer to solving the problem.

Using typeclasses to place constraints

There are Monad laws that you need to adhere to in order for a higher kinded type to have a Monad implementation (left identity, right identity and associativity) but I won’t go into that here because that’s an entire topic in itself.

“Constraints Liberate, Liberties Constrain” — Runar Bjarnason

Imposing constraints on F by demanding it has a Monad implementation

We use the Monad typeclass from the functional library Cats. I just want to emphasize that we’re saying here that F has a Monad typeclass implementation and this is why we are able to use flatMap, map and pure. The code above is actually quite verbose. There is an alternate form known as interface syntax which augments the F effect so it appears as if flatMap and map can be invoked on instances of F[A] but under the hood it’s using implicit classes to make this happen. All you do is import cats.syntax.all._ and it brings a set of implicit classes into scope which enrich any instance of a type that has a Monad implementation and will allow you to invoke flatMap, map and pure. Since we no longer explicitly use the monad instance anymore, we use the more succinct context bound syntax to perform the typeclass constraint on F.

A cleaner looking API thanks to interface syntax

So far, we have written a Service that makes use of a Repository. The Service is abstract in that it expects that the Repository’s effect (F) has a Monad typeclass constraint which gives us the ability to sequence effectful operations in order to provide business functionality built atop the Repository. The real power of tagless final is that you can implement different OrderRepository’s with different Fs (as long as they have Monad implementations) and you can easily swap out different implementations and continue to have the business functionality you built atop the repository continue to work.

A simple in-memory implementation

An in-memory implementation of Order Repository that uses the Id Monad

Let’s see how we can wire this up to our service

Now you may be wondering where the Monad implementation for the Id effect is. I apologize for not showing this here. I have an import pulling that in. I’ll include the source code at the end so you can play around with the code yourself.

A DynamoDB Future based implementation

A Future based implementation that makes use of DynamoDB (built on Scanamo)

Here’s how you wire it up to the service

Again, I apologize for not showing the import. You need to pull in cats.instances.future._ and have an Execution Context in scope to bring the Monad[Future] implementation into scope to meet the constraints. As you can see, this is really similar to the in-memory implementation. The only slight difference is now you start to deal with some more intricacies behind the effect you decided to pick.

A SQL Monix Task based implementation

A Monix Task based implementation that makes use of Postgres (built on Slick)

Here’s how you wire this implementation up

Again, just some database setup followed by the standard usage. Here I bring in monix.cats._ to get a Monad[Task] implementation.

We have to write a very minimal amount of code and we don’t need to make ripples through the entire codebase because of the level of abstraction we have selected. This makes it easy to contrast different implementations since we commit to an implementation right at the end of the program but we ensure that we stay abstract throughout the rest of the program. By placing typeclass constraints on the service, we are able to demand more capabilities from the effect (like flatMap, map and pure when we selected Monad).

A word on constraints and granularity

Granular constraints

Here fetchOrders can be done in parallel so we ask for an Applicative constraint but updateOrder requires sequencing so we ask for a Monad constraint. As you can see, you can pick and choose constraints at a granular level. Of course, the effect used by the repository needs to meet these constraints that are imposed by the service functions in order to be wired up correctly.

Conclusion

Source code

I also refer to source code from my other project which you can find here: