Connecting...

Pexels Photo 335393

Real world Tagless Final: Akka-Http by Basement Crowd

Pexels Photo 335393

How do interpreter patterns fit into application code?

Basement Crowd use the framework Akka-Http and in this article, they give us an insight into how they make Tagless Final work with this framework. 

 

'So far in our series about Interpreter patterns, we have looked at the ideas behind Tagless Final and Free monads, and seen how powerful they can be for de-coupling our application code and abstracting our effect types. However, we haven’t really looked at how these patterns really fit into our application code.

 

Tagless Final & Akka-Http

At Basement Crowd, Akka-Http is the framework that we primarily use for building microservices, so let’s take a look at a simple example of how Tagless Final can work with Akka-Http.

Building akka-http applications with the Tagless Final pattern is mostly, pretty simple. As we previously mentioned, Tagless Final draws parallels both in implementation and benefits of normal abstraction of components into abstract traits, allowing us to decouple the actual implementations and inject appropriate implementation given our use case (for example, a dummy implementation for testing).

For example, let’s imagine we have a really simple document API endpoint that lets your retrieve a list of documents (let’s say PDFs that we have stored by UUID).

At the lowest level we might have a base trait that deals with retrieving our files:

1 trait DocumentRepository[F[_]] {
2   def documents: F[Seq[Document]]
3 }
This trait is responsible for retrieving our files from where we might store them, notice again that we have parameterised the trait with Scala’s higher kinded type notation F[_], this allows us to abstract our effect type. In our code we might have an AWS S3 implementation, or a database implementation that we actually use in production:
1 trait S3DocumentRepository[Future] {
2   def documents:Future[Seq[Document]] = ???
3 }

Here our implementation will go and deal with the AWS SDK and retrieve the PDFs from S3 – and in this example we wrap that in a Future effect type, so we are non blocking (there are lots of other options we could choose from for our effect type, but this is just for example).

Hopefully straight forward so far, we can define out effect per specific implementation which means we can also define test implementations that just use the Identity effect type, for example.

Now, we have this abstract trait that we want to inject into elsewhere in our application – maybe it would go in to some service layer components, and be composed with business logic and other low level calls to build sensible functions, but in our case, for the sake of simplicity, we will just wire this straight into our API akka-http route endpoint for use there.

Now, in our route we would like to keep using this abstract trait, rather than having to specify an implementation, otherwise our routing will be forever coupled with a specific implementation, which would undo all the good work we have done with our abstract trait, so let’s add the same pattern to our routing class:

1 trait DocumentRoute[F[_]] extends DocumentRepository[F]{
2   def F: Monad[F]
3 
4   val getDocuments: Route = (get & path("documents")) { 
5       complete(
6         documents()
7       )
8   }

Above, our route definition is now Tagless Final friendly, and the effect type is abstracted out here too – this means that if we want to test our route, we can still pass in any implementation and effect type that we choose to simplify our testing – finally, the main application class that starts up our akka-http application and web server is the only code that needs to specify the actual implementation that the server will run for real (so the App code that starts up the API server simply acts a bit like config, wiring together the implementations needed).

But wait a minute. Akka-Http is pretty clever at handling many response types, but here we are asking the complete(..) method to handle an as of yet undefined type, F. Akka-Http’s complete(..) method expects to be passed something that it can marshal to a response (specifically using the magnet pattern looking for anything that has a ToResponseMarshaller type class) and whilst it provides ToResponseMarshaller support for Futures and many other things, it can’t guarantee that it will support every possible thing that you might provide it in F, so if we try to compile at this point, the compiler will object.

So how can we get around this? We can of course use the same pattern that we have previously looked at using to make sure we can compose whatever F might be in nice for comprehensions. Remember this line:

def F: Monad[F]

This makes sure that we have some Monad of parameter type F, so any F that we provide, we also need to make sure we provide (implicitly or otherwise) Monad[F] as well. With this constraint in place we know that we can use the monadic properties of any F that could possibly be provided.

So how do we achieve the same for Akka-Http? We simply need to define our own typeclass that will make sure that for any F we provide, we also have support for ToResponseMarshaller. So let’s start by defining the trait for our typeclass:

1 trait MarshallableMonad[F[_]] {
2   def marshaller[A: ToResponseMarshaller]: ToResponseMarshaller[F[A]]
3 }

This typeclass will ensure that for any F, where the inner, wrapped type (A) has an appropriate ToResponseMarshaller, we also have a ToResponseMarshaller for F[A].

Now lets add some default implementations:

1 object MarshallableMonad {
2   implicit def marshaller[F[_], A: ToResponseMarshaller](implicit m: MarshallableMonad[F]): ToResponseMarshaller[F[A]] = m.marshaller
3
4   implicit val futureMarshaller: MarshallableMonad[Future] = new MarshallableMonad[Future] {
5     def marshaller[A: ToResponseMarshaller]: Marshaller[Future[A], HttpResponse] = implicitly
6   }
7   implicit val idMarshaller: MarshallableMonad[Id] = new MarshallableMonad[Id] {
8     def marshaller[A: ToResponseMarshaller]: Marshaller[Id[A], HttpResponse] = implicitly
9   }
10 }

The above type classes simply ensure that if F is a Future or an Id type, and we have a wrapped type of A, which we know we can marshall (a simple type like String, that Akka-Http can handle easily) then we can handle them.

With this new typeclass, we can add this as a constraint to our route component, which will ensure that for any F that is ever provided to the component, there will also be an implementation provided – this gives the assurances required to the compiler to know that any provided will adhere to this constraint and will have a ToResponseMarshaller:

1 trait DocumentRoute[F[_]] extends DocumentRepository[F]{
2   def F: Monad[F]
3   def M: MarshallableMonad[F]
4
5   val getDocuments: Route = (get & path("documents")) { 
6       complete(
7         documents()
8       )
9   }

 

Conclusion

Obviously this is not a full blown production application in terms of complexity or all elements of the code structure, however, it does show how the Tagless Final pattern can work nicely with an Akka-Http application and how you can decouple the effect types and implementation details to the very outer wrapper of the application (or test) code.'

 

This article was written by and originally posted by Basement Crowd.