Toby Hobson

The Tagless Final Pattern

banner.png
The Tagless Final Pattern
Estimated reading time: 4 minutes

Tagless Final is something you may have heard about. It’s a functional programming pattern but what exactly is it?

Introduction

Before getting into the details I want to share with you a word of warning. Tagless final may sound like a cool pattern but it’s not a panacea. I recommend you read John de Goes post highlighting the pitfalls of this pattern. In particular I would urge you to avoid “premature indirection”. Now I’ve got the health warning out of the way let’s crack on …

The problem we aim to solve

Most people are familiar with the concept of dependency injection. We can inject some database code lookup logic into a service. Take this Java style class:

trait UserRepo {
  def getUser(id: Long): Future[Option[User]]
}

class UserService(userRepo: UserRepo) {
  def doStuff(userId: Long): Future[Int] = { userRepo.getUser(userId) ... }
} 

Actually let’s make look a little less Java and a bit more Scala …

trait UserRepo {
  def getUser(id: Long): Future[Option[User]]
}

object UserService {
  def doStuff(userId: Long)(implicit userRepo: UserRepo): Future[Int] = { userRepo.getUser(userId) ... }
} 

If we want to unit test our service we can of course stub out the UserRepo but the return type of getUser must be Future[Option[User]]. This means:

  1. We need to write something like Future.successful(...) in our stub
  2. As doStuff also returns a Future we need write something like Await.result(result, N.seconds), we need an ExecutionContext in scope etc etc.

We also can’t easily replace Future with Monix Task or Cats IO

Abstracting over F

The Tagless Final pattern allows us to replace Future with a polymorphic effect type. By convention we call this F (for Functor) or M (for Monad). i.e. UserRepo becomes:

trait UserRepo[F[_]] {
  def getUser(id: Long): F[Option[User]]
}

F is a “Higher Kinded Type”, think of it as a type that contains or “wraps” another type. This is what the F[_] syntax means. So how do we inject our new abstract repo into the service? We also make the service polymorphic as to the effect:

object UserService {
  def doStuff[F[_]](userId: Long)(implicit userRepo: UserRepo[F]): F[Int] = { userRepo.getUser(userId) ... }
} 

We now need an UserRepo implementation for a given “effect”. Let’s write one using Future as our effect:

class FutureUserRepo extends UserRepo[Future] {
  def getUser(id: Long): Future[Option[User]] = ???
}

We can now call our UserService, implicitly passing our Future based repo:

implicit val userRepo = new FutureUserRepo
val eventualResult: Future[Int] = UserService.doStuff[Future](1L) // userRepo is passed implicitly
val result = Await.result(eventualResult, 1.second)

Ok, we haven’t really achieved anything here but we can …

class IdUserRepo extends UserRepo[Id] {
  def getUser(id: Long): Id[Option[User]] = Some(User(...))
}

implicit val userRepo = new IdUserRepo
val result: Int = UserService.doStuff[Id](1L)

Id is a really useful type from the Cats ecosystem. It is basically a no-op “F”

Mapping and FlatMapping over F

My UserService is incomplete. It just fetches a user from the repo, does “something” and returns an Int wrapped in the given effect. If UserService uses Future as the hard coded effect we can easily map and flatMap over the response from the repo as we’re dealing with a Future

object UserService {
  def doStuff(userId: Long)(implicit userRepo: UserRepo): Future[Int] = { 
    userRepo.getUser(userId).map(user => user.age)
  }
}

How do we do this if F is abstract? Helpfully Cats includes the Functor type class and we can specify that F must have an implementation:

object UserService {
  def doStuff[F[_]](userId: Long)(implicit functor: Functor[F], userRepo: UserRepo[F]): F[Int] = {
    functor.map(userRepo.getUser(userId))(_.age)
  }
} 

we can make this look a bit cleaner using some “syntax” help:

object UserService {
  import cats.syntax.functor._

  def doStuff[F[_]](userId: Long)(implicit functor: Functor[F], userRepo: UserRepo[F]): F[Int] = {
    userRepo.getUser(userId).map(_.age)
  }
} 

Of course this is just an example. We can use Monad and cats.syntax.flatmap._ to sequence effectful operations, Applicative to work with independent effectful operations etc.

Summary

The Tagless Final pattern allows us to replace hard codes “effects” like Future with a generic F[_].

comments powered by Disqus

Need help with your project?

Do you need some help or guidance with your project? Reach out to me (email is best)