scalatypeclassimplicittagless-final

Tagless final example in Scala requires superfluous second interp arg


I'm playing around with implementing a tagless final DSL & interpreter in Scala, based on this blog post written in Haskell.

I can get an example running - see code below, but I don't quite understand why I need testVal(Interp)(Interp). If I only supply a single Interp argument, then I get the following compile errors:

Error:(29, 24) could not find implicit value for evidence parameter of type Test.Expr[Test.Id]
        val x = testVal(Interp)
Error:(29, 24) not enough arguments for method testVal: (implicit evidence$1: Test.Expr[Test.Id])Test.Id[Int].
Unspecified value parameter evidence$1.
        val x = testVal(Interp)

Is there a simple way to eliminate one of the Interp arguments?

object Test {
    trait Expr[F[_]] {
        def const(i: Int): F[Int]
        def lam[A, B](f: F[A] => F[B]): F[A => B]
        def app[A, B](f: F[A => B], a: F[A]): F[B]
        def add(x: F[Int], y: F[Int]): F[Int]
    }

    type Id[A] = A

    object Interp extends Expr[Id] {
        override def const(i: Int): Id[Int] = i
        override def lam[A, B](f: Id[A] => Id[B]): Id[A => B] = f
        override def app[A, B](f: Id[A => B], a: Id[A]): Id[B] = f(a)
        override def add(x: Id[Int], y: Id[Int]): Id[Int] = x + y
    }

    def testVal[F[_]: Expr](f: Expr[F]): F[Int] =
        f.app(
            f.lam[Int, Int](
                x => f.add(x, f.const(1))),
            f.const(10)
        )

    def main(args: Array[String]): Unit = {
        // val x = testVal(Interp) -- won't compile
        val x = testVal(Interp)(Interp)
        println(x)
    }
}

Solution

  • The syntax

    def f[X: Y](args: Types): Res = { ... }
    

    is a shortcut for

    def f[X](args: Types)(implicit yx: Y[X]): Res = { ... }
    

    so if you write

    def testVal[F[_]: Expr](f: Expr[F]): F[Int] = { ... }
    

    then it's the same as if you wrote

    def testVal[F[_]](f: Expr[F])(implicit redundant: Expr[F]): F[Int] = { ... }
    

    but you obviously don't need the same Expr[F] twice.

    The signature should be either

    def testVal[F[_]: Expr]: F[Int]
    

    or

    def testVal[F[_]](implicit f: Expr[F]): F[Int]
    

    but not both at the same time.

    Here is a full example, which also shows how to get the f using implicitly in the case that you decide to use the F: Expr variant (which does not assign a name to the implicit argument):

    import scala.language.higherKinds
    
    object Test {
        trait Expr[F[_]] {
            def const(i: Int): F[Int]
            def lam[A, B](f: F[A] => F[B]): F[A => B]
            def app[A, B](f: F[A => B], a: F[A]): F[B]
            def add(x: F[Int], y: F[Int]): F[Int]
        }
    
        type Id[A] = A
    
        object Interp extends Expr[Id] {
            override def const(i: Int): Id[Int] = i
            override def lam[A, B](f: Id[A] => Id[B]): Id[A => B] = f
            override def app[A, B](f: Id[A => B], a: Id[A]): Id[B] = f(a)
            override def add(x: Id[Int], y: Id[Int]): Id[Int] = x + y
        }
    
        def testVal[F[_]: Expr]: F[Int] = {
            implicit val f = implicitly[Expr[F]]
            f.app(
                f.lam[Int, Int](
                    x => f.add(x, f.const(1))),
                f.const(10)
            )
        }
    
        def main(args: Array[String]): Unit = {
            val x = testVal(Interp)
            println(x)
        }
    }
    

    Moreover, if you make Interp itself implicit, then you can omit all argument lists when invoking testVal, and instead write just

    val x = testVal // no arguments at all.