Search code examples
scalashapelessscala-macros

How to reduce boilerplate code with Scala Macros in Scala 2?


For such code, there are many boilerplate code.

object TupleFlatten {
  import shapeless._
  import ops.tuple.FlatMapper
  import syntax.std.tuple._

  trait LowPriorityFlat extends Poly1 {
    implicit def default[T] = at[T](Tuple1(_))
  }

  object Flat extends LowPriorityFlat {
    implicit def caseTuple1[P <: Tuple1[_]](implicit fm: FlatMapper[P, Flat.type]): Flat.Case[P] {
      type Result = FlatMapper[P, Flat.type]#Out
    } =
      at[P](_.flatMap(Flat))

    implicit def caseTuple2[P <: Tuple2[_, _]](implicit fm: FlatMapper[P, Flat.type]) =
      at[P](_.flatMap(Flat))

    implicit def caseTuple3[P <: Tuple3[_, _, _]](implicit fm: FlatMapper[P, Flat.type]) =
      at[P](_.flatMap(Flat))

    implicit def caseTuple4[P <: Tuple4[_, _, _, _]](implicit fm: FlatMapper[P, Flat.type]) =
      at[P](_.flatMap(Flat))
  }
}

Is there any way to auto generate code like following, from Tuple1 to Tuple22

implicit def caseTupleN[P <: TupleN[???]](implicit fm: FlatMapper[P, Flat.type]) =
      at[P](_.flatMap(Flat))

How to do that?


Solution

  • Surely, you can generate implicits for example with macro annotation

    import scala.annotation.{StaticAnnotation, compileTimeOnly}
    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    object Macros {
    
      @compileTimeOnly("enable macro annotations")
      class genImplicits(n: Int) extends StaticAnnotation {
        def macroTransform(annottees: Any*): Any = macro genImplicitsMacro.impl
      }
    
      object genImplicitsMacro {
        def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
          import c.universe._
          val n = c.prefix.tree match {
            case q"new genImplicits(${n1: Int})" => n1
          }
          val implicits = (1 to n).map { k =>
            val undescores = Seq.fill(k)(tq"${TypeName("_")}")
            q"""
              implicit def ${TermName("caseTuple" + k)}[P <: _root_.scala.${TypeName("Tuple" + k)}[..$undescores]: _root_.shapeless.IsTuple](implicit
                fm: _root_.shapeless.ops.tuple.FlatMapper[P, this.type]
              ): this.Case.Aux[P, fm.Out] = this.at[P].apply[fm.Out](_.flatMap(this))
            """
          }
          annottees match {
            case q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" :: Nil =>
              q"""
                $mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
                  import _root_.shapeless.syntax.std.tuple._
                  ..$implicits
                  ..$body
                }
              """
          }
        }
      }
    }
    
    import Macros.genImplicits
    import shapeless.Poly1
    
    trait LowPriorityFlat extends Poly1 {
      implicit def default[T]: Case.Aux[T, Tuple1[T]] = at[T](Tuple1(_))
    }
    
    @genImplicits(4)
    object Flat extends LowPriorityFlat
    
    Flat(((1, (2, 3), (4, (5, 6, 7))), (8, 9))) // (1,2,3,4,5,6,7,8,9)
    

    Compilation of Flat(((1, (2, 3), (4, (5, 6, 7))), (8, 9))) with @genImplicits(22) takes too long although expansion of @genImplicits(22) itself is pretty fast.

    Alternatively you can use code generation with Shapeless Boilerplate, Scala genprod, sbt-boilerplate or Scalameta.

    But I can't see how this will be better than a simpler definition using just Shapeless with context bound IsTuple rather than upper bound

    import shapeless.ops.tuple.FlatMapper
    import shapeless.{IsTuple, Poly1}
    import shapeless.syntax.std.tuple._
    
    trait LowPriorityFlat extends Poly1 {
      implicit def default[T]: Case.Aux[T, Tuple1[T]] = at[T](Tuple1(_))
    }
    
    object Flat extends LowPriorityFlat {
      implicit def caseTuple[P: IsTuple](implicit fm: FlatMapper[P, Flat.type]): Case.Aux[P, fm.Out]  =
        at[P](_.flatMap(Flat))
    }
    
    Flat(((1, (2, 3), (4, (5, 6, 7))), (8, 9))) // (1,2,3,4,5,6,7,8,9)
    

    or even without bounds

    import shapeless.ops.tuple.FlatMapper
    import shapeless.{IsTuple, Poly1}
    
    trait LowPriorityFlat extends Poly1 {
      implicit def default[T]: Case.Aux[T, Tuple1[T]] = at[T](Tuple1(_))
    }
    
    object Flat extends LowPriorityFlat {
      implicit def caseTuple[P](implicit fm: FlatMapper[P, Flat.type]): Case.Aux[P, fm.Out]  =
        at[P](fm(_))
    }
    
    Flat(((1, (2, 3), (4, (5, 6, 7))), (8, 9))) // (1,2,3,4,5,6,7,8,9)
    

    Please notice that using return type of implicits with type projections Flat.Case[P] { type Result = FlatMapper[P, Flat.type]#Out } aka Flat.Case.Aux[P, FlatMapper[P, Flat.type]#Out] can sometimes lead to issues with implicit resolution (unless you know what you're doing). It's better to use path-dependent types in return type of implicits Flat.Case.Aux[P, fm.Out].