Search code examples
scalascala-macroslensesscala-macro-paradisescalameta

Scala recursive macro?


I was wondering whether Scala supports recursive macro expansion e.g. I am trying to write a lens library with a lensing macro that does this:

case class C(d: Int)
case class B(c: C)
case class A(b: B)

val a = A(B(C(10))
val aa = lens(a)(_.b.c.d)(_ + 12)
assert(aa.b.c.d == 22)

Given lens(a)(_.b.c.d)(f), I want to transforms it to a.copy(b = lens(a.b)(_.c.d)(f))

EDIT: I made some decent progress here

However, I cannot figure out a generic way to create an accessor out of List[TermName] e.g. for the above example, given that I have List(TermName('b'), TermName('c'), TermName('d'))), I want to generate an anonymous function _.b.c.d i.e. (x: A) => x.b.c.d. How do I do that?

Basically, how can I write these lines in a generic fashion?


Solution

  • Actually I managed to make it work: https://github.com/pathikrit/sauron/blob/master/src/main/scala/com/github/pathikrit/sauron/package.scala

    Here is the complete source:

    package com.github.pathikrit
    
    import scala.reflect.macros.blackbox
    
    package object sauron {
    
      def lens[A, B](obj: A)(path: A => B)(modifier: B => B): A = macro lensImpl[A, B]
    
      def lensImpl[A, B](c: blackbox.Context)(obj: c.Expr[A])(path: c.Expr[A => B])(modifier: c.Expr[B => B]): c.Tree = {
        import c.universe._
    
        def split(accessor: c.Tree): List[c.TermName] = accessor match {    // (_.p.q.r) -> List(p, q, r)
          case q"$pq.$r" => split(pq) :+ r
          case _: Ident => Nil
          case _ => c.abort(c.enclosingPosition, s"Unsupported path element: $accessor")
        }
    
        def join(pathTerms: List[TermName]): c.Tree = (q"(x => x)" /: pathTerms) {    // List(p, q, r) -> (_.p.q.r)
          case (q"($arg) => $pq", r) => q"($arg) => $pq.$r"
        }
    
        path.tree match {
          case q"($_) => $accessor" => split(accessor) match {
            case p :: ps => q"$obj.copy($p = lens($obj.$p)(${join(ps)})($modifier))"  // lens(a)(_.b.c)(f) = a.copy(b = lens(a.b)(_.c)(f))
            case Nil => q"$modifier($obj)"                                            // lens(x)(_)(f) = f(x)
          }
          case _ => c.abort(c.enclosingPosition, s"Path must have shape: _.a.b.c.(...), got: ${path.tree}")
        }
      }
    }
    

    And, yes, Scala does apply the same macro recursively.