Revisiting Tagless Final Interpreters
   
    Tageless Final interpreters are an alternative to the traditional Algebraic Data Type (and generalized ADT) based implementation of the interpreter pattern. This document presents the Tageless Final approach with Scala, and shows how Dotty with it’s recently added implicits functions makes the approach even more appealing. All examples are direct translations of their Haskell version presented in the
    
     Typed Tagless Final Interpreters: Lecture Notes
    
    (section 2).
   
    The interpreter pattern has recently received a lot of attention in the Scala community. A lot of efforts have been invested in trying to address the biggest shortcomings of ADT/GADT based solutions: extensibility. One can first look at cats’
    
     
      Inject
     
     typeclass
    
    for an implementation of
    
     
      Data Type à la Carte
     
    
    ideas. The
    
     Freek
    
    library provides facilities to combine more than two algebras using some pretty involved type-level machinery. The solution proposed by the
    
     Freer Monads, More Extensible Effects
    
    paper also focuses on extensibility, and inspired a bunch of work of small Scala libraries such as
    
     eff
    
    ,
    
     emm
    
    and
    
     paperdoll
    
    . Tagless Final interpreters take a somewhat of a dual approach by having typeclasses at their very core instead of the more traditional ADT/GADT. They also come with the great advantage of being out of the box extensible without having any apparent pitfall.
   
    The
    
     lecture notes
    
    goes into great details in presenting and comparing both ADT/GADT and typeclass based approaches, referencing the former as
    
     initial
    
    and the later as
    
     final
    
    . For the sake of conciseness, this document focuses mainly on
    
     final
    
    interpreters.
   
    
    
    Introduction
   
We will be working with simple mathematic expressions similar to the ones manipulated on calculators. Our task will not only consist in computing these expressions, but also serialize, deserialize, and simplify them. As lazy engineers, it makes perfect sense to represent the domain using an (embedded) domain specific language. Among other things this saves us from caring about all the possible incorrect representations of our domain: the host language compiler takes care of that for us. Our domain consists in integer literals with addition and negation. The encoding that might have popped right up into your mind might look like the following:
sealed trait IExp
final case class Lit(i: Int) extends IExp
final case class Neg(e: IExp) extends IExp
final case class Add(r: IExp, l: IExp) extends IExp
    A mathematical expression such as
    
     8 - (1 + 2)
    
    can be encoded as a value of type
    
     IExp
    
    :
   
val fe: IExp = Add(Lit(8), Neg(Add(Lit(1), Lit(2))))
    Now everything looks easy right? Interpreting
    
     fe
    
    as integer uses recursive function of type
    
     IExp => Int
    
    , serializing is done with a
    
     IExp => Json
    
    function, deserialize goes the other way around with
    
     Json => Option[IExp]
    
    , and transformations are done
    
     IExp => IExp
    
    functions.
   
    In the lecture notes’ terminology, the
    
     IExp
    
    data type corresponds to the
    
     initial
    
    encoding of our domain. You can forget about it for now, as we will instead be using the
    
     final
    
    encoding:
   
trait Exp[T] {
   
  def lit(i: Int): T
  def neg(t: T): T
  def add(l: T, r: T): T
}
    How do we represent
    
     8 - (1 + 2)
    
    with
    
     Exp
    
    ? Something like the following:
   
def tf0[T](implicit e: Exp[T]): T =
  e.add(e.lit(8), e.neg(e.add(e.lit(1), e.lit(2))))
    In Haskell (with proper language extension)
    
     tf0
    
    could be a polymorphic value. In Scala we use a function with a type parameter
    
     T
    
    and an
    
     implicit Exp[T]
    
    constraint. The syntax can be simplified (for example) by getting rid of the
    
     e.
    
    using helper functions:
   
object ExpSyntax {
   
  def lit[T](i: Int)    (implicit e: Exp[T]): T = e.lit(i)
  def neg[T](t: T)      (implicit e: Exp[T]): T = e.neg(t)
  def add[T](l: T, r: T)(implicit e: Exp[T]): T = e.add(l, r)
}
import ExpSyntax._ // It's safe to always have these in scope
def tf1[T: Exp]: T =
  add(lit(8), neg(add(lit(1), lit(2))))
    At this point you probably wonder,
    
     how do we write interpreters for this
     
      tf1
     
     thing?
    
    . The answer is simple, by creating instances of
    
     Exp
    
    !
   
implicit val evalExp: Exp[Int] = new Exp[Int] {
   
  def lit(i: Int): Int = i
  def neg(t: Int): Int = -t
  def add(l: Int, r: Int): Int = l + r
}
implicit val printExp: Exp[String] = new Exp[String] {
   
  def lit(i: Int): String = i.toString
  def neg(t: String): String = s"(-$t)"
  def add(l: String, r: String): String = s"($l + $r)"
}
    Interpretation is done by specifying types. Let’s looks at
    
     tf1
    
    as an
    
     Int
    
    :
   
scala> tf1[Int]
res0: Int = 5
    What about
    
     tf1
    
    as a
    
     String
    
    ?
   
scala> tf1[String]
res1: String = (8 + (-(1 + 2)))
    
    
    Extensibility
   
    What if we decide to extend our mathematical expressions with multiplication? With the
    
     initial
    
    (ADT based)
   
 
