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:
- We need to write something like
Future.successful(...)
in our stub - As
doStuff
also returns aFuture
we need write something likeAwait.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[_].