Because Tagless Final is gaining a lot of traction those years, here is a memo for the syntax in Scala. Actually it is mostly about typeclasses encoding and implicits.

In this memo, we are assuming the following imports:

import cats._
import cats.data._
import cats.implicits._

We also assume Scala 2.1x.y as the typeclass encoding will change in Scala 3.

Requiring a typeclass instance

Instances are bound by implicits. There are two ways to do so. Here is an example of a function requiring cats.Functor.

Using implicit parameters

def mapAndKeep[F[_], A, B](
    fa: F[A]
  )(f: A => B)(implicit FF: Functor[F]): F[(A, B)] =
    FF.map(fa) { a =>
      (a, f(a))
    }

No surprises, but maybe long to write...

Using context bounds & implicitly

def `mapAndKeep'`[F[_]: Functor, A, B](fa: F[A])(f: A => B): F[(A, B)] =
    implicitly[Functor[F]].map(fa) { a =>
      (a, f(a))
    }
  • F[_]: Functor clearly states the conditions on F
  • implicitly[Functor[F]] is a Scala utility function available in Predef :
@inline def implicitly[T](implicit e: T): T = e

Clever trick done by cats

The version using implicitly can be simplified if the author of the typeclass wrote something like this in the companion object:

object Functor {
  def apply[F[_]](implicit instance: cats.Functor[F]) = instance
}

Which you can use as such :

def mapAndKeep[F[_]: Functor, A, B](fa: F[A])(f: A => B): F[(A, B)] =
  Functor[F].map(fa) { a =>
    (a, f(a))
  }

Bonus, the apply function is easily derivable.

Adding direct syntax to F instances

This is done to achieve some thing like the method with receiver idiom in Kotlin (sample from Arrow which add map to every type having a Functor instance):

fun <A, B> Kind<F, A>.map(f: (A) -> B): Kind<F, B>

In Scala this is done via implicit classes.
Let's say we have the following typeclass (it is completely meaningless):

trait SomeTC[F[_]] {

  def someFn[A, B, G[_]: Functor](a: F[A], gb: G[B]): F[(A, B)]

}

object SomeTC {

  implicit val optionSomeTC: SomeTC[Option] = ???

  implicit class SomeTCSyntax[F[_]: SomeTC, A](instance: F[A]) {
    def someFn[B, G[_]: Functor](gb: G[B]) =
      implicitly[SomeTC[F]].someFn(instance, gb)
  }

}

Then, by importing the implicit SomeTCSyntaxclass, we can write the following:

val o = 42.some
val b: List[String] = List("scala", "kotlin", "java")
val r: Option[(Int, String)] = o.someFn(b)

If you dig into cats code source, you will not see much implicit classes (you cannot put them into a trait), but a bunch of implicit def which in this case are implicit conversions. Those implicit conversions produce Ops objects stating clearly their extension nature. Then, Cats uses a clever stacking of traits to achieve the extension method pattern.

Reasoning on an abstract F

The hardest part when starting to write code in tagless final style is the need to get our brain into the abstract level. I mean, to clearly know the limitation required on that F. Sometimes, it requires to know the typeclass hierarchy to follow the least power principle. That way, we keep functions as generic as possible (and widen the number of types for which an implementation is possible). The Glossary in Cats and the Cats-effect typeclasses reference are the go to places in case of doubt.

In Scala, remember, that there is no way to completely be sure that a given function does not mess up with the outside world (ie performs side effects). Discipline ensures that such a simple function like:

def compose[A, B,C](f: B => C, g: A => B): A => C  

has only one implementation. In reality, any Scala developer is completely free to write an impure version without a change in the signature :

def compose[A, B,C](f: B => C, g: A => B): A => C  = a => {
    new Thread {/* messing up like crazy */}
    f(g(a))
}

Also, keep in mind that Tagless Final is no silver bullet. It has his detractors & advocates...

Do you want more ?

Well, this post is just a raw summary, but if you want to dig more, check this awesome blog post by Mateusz Kubuszok.

Mateusz Kubuszok also wrote an excellent article which is worth reading if you want to know more about Tagless Final, Free monads, ZIO,....

This gist by Didier Plaindoux also sums it up.