An introduction to Tagless Final in Scala
When you are building any kind of non-trivial application, you will naturally find yourself needing to decouple your implementation details from your application. Loose coupling not only makes code easier to test but also makes it easier to switch implementations in the future or for different use cases. One pattern that is increasing in popularity in the Scala community over the last two years is known as tagless final, and is a pattern we have started to make fairly extensive use of here at Basement Crowd (another similar approach is to use Free Monads, which we will talk about in another future post).
We first came acorss some of the core concepts of the pattern in the form of The Abstract Future. Originally we started using the technique to make it easier to test our database layer, using Scala’s higher kinded types to abstract the DBIO and Future type constructors away from our traits under test. Which makes sense really, the fact that an implementation wraps its result in a Future, IO or DBIO etc is really an implementation detail that we don’t necessarily need to leak into the rest of the application code. Furthermore, it means at testing we can substitute a mock implementation that doesn’t need to deal with those specific type constructors and could just be a simple Id monad for example.
Tagless Final: A Functional Programmer’s approach
Technically speaking, the tagless final pattern is an implementation of the Interpreter pattern. This pattern is made up of two aspects:
- A Domain Specific Language (DSL)
- An interpreter
Our DSL could be written in a couple of different ways, one is where the DSL is modelled using Algebraic Data Types (ADT) and an alternative is a DSL modelled using abstract method definitions in a trait. The ADT representation of the DSL is used when working with Free monads, tagless final on the other hand represents the DSL as a parameterised trait with abstract method definitions.
Let’s consider the a real life case from an application we have – some code to manage ElasticSearch indexes. You don’t need to worry about knowing ElasticSearch, just that there is a concept known as an index that we need to create, delete, update etc, and that those operations happen via the API of a running ElasticSearch instance. To start, we will model a small subset of our ElasticSearch Index DSL: creating and deleting indexes
The above definition of those abstract methods represents the subset of our DSL that lets us create a new ElasticSearch Index, and delete existing Indexes. Note, these abstract methods we have defined here can be composed as necessary, and also that the response types are wrapped in an “F” type – this is Scala’s higher kinded type notation and allows us to abstract both the method implementation and the type constructor wrapper around the return type of those methods. F can be thought of as our effect type, for example a Future, IO, Task etc. that will be defined by the interpreter. This is a central part of the tagless final pattern.
Now we have our DSL, we can think about using the DSL to describe programs. Let’s try to create a program that will recreate our index (basic composition of our DSL of deleting any existing index of that name initially, and then creating it new again):
The above program defines the logic to delete and recreate our ElasticSearch Index using our new DSL, agnostic to the interpreter that is provided and to the actual type F that is provided. There are two interesting points to note here:
- [F[_]: Monad] – this context bound ensures that there is an implicit Monad[F] in scope, meaning we can safely deal with our generic F type as a monad and use the powerful tools available, such as for-comprehension (as in this case). The Monad typeclass is provided by the Cats library.
- implicit interpreter: IndexDsl[F] – at invocation, we also make sure that there is an implicit IndexDsl[F] in scope, this gives us a compile time guarantee that we will have an interpreter to hand that can deal with any type F that we try to invoke the method for.
With those two constraints on our code, we can safely build programs using this DSL regardless of what F is provided. This is a very simple DSL and sample program, but you can imagine with a fully fledged DSL you could build fairly sophisticated programs. As we have abstracted the effect type, so it is just an implementation detail, its makes it very easy to build programs from multiple different DSLs, as long as they have the same F, that is, if you have an IndexDsl[F] and some DatabaseDsl[F], you can build for comprehensions easily mixing the DSLs (for example, if you need to update ElasticSearch, access a database and make HTTP service calls, they can all have independent DSLs with a shared effect type, F).
Obviously, at this point our program built with a DSL is not much use without an interpreter provided. This is simple as implementing the IndexDsl trait for the relevant type constructors, F, that we want to support. In reality, in our production code, to interact with ElasticSearch we need to use the HTTP API, so we would want something sensible to wrap those calls and run them asynchronously. Below, we are implementing these interpreters as type classes, but that isn’t a requirement of the pattern.
To execute our program, we can simply call the method with the type:
Tagless Final: A simple layer of abstraction
If you are coming from a more OO background (like from Java) then you may have been reading about tagless final, about DSLs and Interpreters, and be struggling to follow all the concepts or how they fit into your wider real-life application code, but the reality is that this pattern is very similar to standard abstraction you might be familiar with elsewhere, but super-charged.
You may be more familiar with a scenario where you have some application code that has a reference to an abstract service component (normally defined as an interface) and makes use of the abstract methods, and the specific details of the actual implementation of the interface can be provided at runtime (traditionally via some Dependency Injection mechanism). This pattern offers similar support – the DSL we defined is simply an interface to the abstract methods, and then the interpreter that we provided as a type class is just an implementation of our interface. The fact that as well as abstracting the method implementation we can also abstract the effect type of our methods is what supercharges it (in reality, tagless final offers more than simply a way to abstract out interface implementations, but the pattern does lend itself well to achieving similar level of abstraction that OO developers maybe more familiar with).
In summary, Tagless final provides a simple and clean pattern to decouple implementation details from your application code, and further, allows generic programs to be re-used with a range of different interpreters as needed for different purposes. In a future post we will look at a simple application of this pattern in an Akka-Http application and in another we will take a look at Free monads and compare the approaches.