Search code examples
scalaimplicitscala-macroscallbyname

Macro variant of implicit class that allows for by-name argument


For a DSL, I want to introduce a dup extension method that basically calls Vector.fill, e.g.

import scala.collection.immutable.{IndexedSeq => Vec}

implicit final class Dup[A](private val in: A) extends AnyVal {
  def dup(n: Int): Vec[A] = Vector.fill(n)(in)
}

3 dup 4  // Vector(3, 3, 3, 3)

Now I want to make the argument a by-name value, so that the following would work correctly:

math.random dup 4  // wrong: four times the same value

I am looking at this question, so apparently there is no solution with a plain value class, only:

final class Dup[A](in: () => A) {
  def dup(n: Int): Vec[A] = Vector.fill(n)(in())
}
implicit def Dup[A](in: => A): Dup[A] = new Dup(() => in)

math.random dup 4   // ok

...undoing the advantage of a value-class that there is no boxing involved.

So I was wondering, would it be possible to write a macro that provides a no-instantiation solution where the argument is by-name?


Solution

  • Why not?

    // Doesn't matter if it's value class or not, code generated by macro
    // will contain no references to it.
    implicit final class Dup[A](in: A) {
      def dup(n: Int): Vec[A] = macro Macros.dupImpl[A]
    }
    object Dup {
      def dup[A](in: => A, n: Int) = Vector.fill(n)(in)
    }
    

    Macro impl:

    import scala.reflect.macros.blackbox
    
    object Macros {
      def dupImpl[A](c: blackbox.Context)(n: c.Expr[Int]): c.Tree = {
        import c.universe._
        val q"$conv($in)" = c.prefix.tree
        q"Dup.dup($in, $n)"
      }
    }
    

    The c.prefix can be assumed to contain a tree for the in argument wrapped in an implicit conversion (we can add some validation code and emit compilation error if it's not). We simply unwrap it and obtain the original tree that represents in. We then pass it directly to Dup.dup completely discarding implicit conversion in the finally generated code.

    The only instantition left will be instantiation of Function0 object that will be passed in place of by-name parameter, but this is unavoidable.