Метапрограммирование на Scala

со Scalameta

Программирование

?

!

Метапрограммирование

?

!

Scalameta

Библиотека для метапрограммирования на Scala

Scalameta

libraryDependencies += "org.scalameta" %% "scalameta" % "2.0.1"

DEMO TIME

case class User(name: String)
Defn.Class(List(Mod.Case()), Type.Name("User"), Nil, 
Ctor.Primary(Nil, Name(""), List(List(Term.Param(Nil, 
Term.Name("name"), Some(Type.Name("String")), None)))), 
Template(Nil, Nil, Self(Name(""), None), Nil))
Defn.Class(
  mods = List(Mod.Case()),
  name = Type.Name("User"),
  tparams = Nil,
  ctor = Ctor.Primary(
    mods = Nil,
    name = Name(""),
    paramss = List(List(
      Term.Param(
        mods = Nil,
        name = Term.Name("name"),
        decltpe = Some(Type.Name("String")),
        default = None
      )
    ))
  ),
  templ = Template(Nil, Nil, Self(Name(""), None), Nil)
)
q"case class User(name: String)"

Quasiquotes

Quasiquotes

val method = q"def upperName: String = name.toUpperCase"
q"""case class User(name: String) {
  $method   
}"""
// meta.Defn.Class = case class User(name: String) { 
//   def upperName: String = name.toUpperCase 
// }

Quasiquotes

q"case class User(name: String)" match {
  case q"case class $className(..$paramss)" =>
    val ageParam = param"age: Int"
    val newParamss = paramss :+ ageParam
    q"case class $className(..$newParamss)"
  case other => other
}
// meta.Defn.Class = 
//  case class User(name: String, age: Int)

Quasiquotes

source"""
  sealed trait Op[A]
  object Op extends B {
    case class Foo(i: Int) extends Op[Int]
    case class Bar(s: String) extends Op[String]
  }
""".collect { 
  case cls: Defn.Class => cls.name 
}
// List[meta.Type.Name] = List(Foo, Bar)

Macro annotations

libraryDependencies += "org.scalameta" %% "scalameta" % "1.8.0"
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-M10" cross CrossVersion.full)
scalacOptions += "-Xplugin-require:macroparadise"

Macro annotations

import scala.annotation.StaticAnnotation
import scala.meta._

class HelloWorld extends StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    defn // macro code
  }
}

Macro annotations

// before
class ToMapTest(a: String, b: Int)(c: Double)
// after
class ToMapTest(a: String, b: Int)(c: Double) {
  def toMap: Map[String, String] = {
    Map(("a", a.toString), ("b", b.toString), ("c", c.toString))
  }
}

Macro annotations

class ToMap extends StaticAnnotation {

  inline def apply(defn: Any): Any = meta {
     
  }
}
class ToMap extends StaticAnnotation {

  inline def apply(defn: Any): Any = meta {
    defn match {

    }
  }
}
class ToMap extends StaticAnnotation {

  inline def apply(defn: Any): Any = meta {
    defn match {
      case cls @ Defn.Class(_, _, _, _, _) =>
      case _ =>
    }
  }
}
class ToMap extends StaticAnnotation {

  inline def apply(defn: Any): Any = meta {
    defn match {
      case cls @ Defn.Class(_, _, _, _, _) =>
      case _ =>
        abort("@ToMap must annotate a class")
    }
  }
}
class ToMap extends StaticAnnotation {

  inline def apply(defn: Any): Any = meta {
    defn match {
      case cls @ Defn.Class(_, _, _, ctor, _) =>
        val tuples: Seq[Term.Tuple] = ctor.paramss.flatten.map { param: Term.Param =>
          q"(${Lit.String(param.name.value)}, ${Term.Name(param.name.value)}.toString)"
        }

      case _ =>
        abort("@ToMap must annotate a class")
    }
  }
}
class ToMap extends StaticAnnotation {

  inline def apply(defn: Any): Any = meta {
    defn match {
      case cls @ Defn.Class(_, _, _, ctor, _) =>
        val tuples: Seq[Term.Tuple] = ctor.paramss.flatten.map { param: Term.Param =>
          q"(${Lit.String(param.name.value)}, ${Term.Name(param.name.value)}.toString)"
        }

        val method: Defn.Def =
          q"""def toMap: _root_.scala.collection.Map[String, String] = {
            _root_.scala.collection.Map[String, String](..$tuples)
          }"""

      case _ =>
        abort("@ToMap must annotate a class")
    }
  }
}
class ToMap extends StaticAnnotation {

  inline def apply(defn: Any): Any = meta {
    defn match {
      case cls @ Defn.Class(_, _, _, ctor, template) =>
        val tuples: Seq[Term.Tuple] = ctor.paramss.flatten.map { param: Term.Param =>
          q"(${Lit.String(param.name.value)}, ${Term.Name(param.name.value)}.toString)"
        }

        val method: Defn.Def =
          q"""def toMap: _root_.scala.collection.Map[String, String] = {
            _root_.scala.collection.Map[String, String](..$tuples)
          }"""

        val templateStats: Seq[Stat] = method +: template.stats.getOrElse(Nil)
        cls.copy(templ = template.copy(stats = Some(templateStats)))

      case _ =>
        abort("@ToMap must annotate a class")
    }
  }
}

Macro annotations

Macro annotations

  • Работает уже сейчас в Idea stable

  • Scala JVM + Scala.js

  • Syntactic only

Macros

ВСЁ СЛОЖНО

Macros

  • scala.reflect - deprecated
  • Scalameta < 2.0.0 + macro-paradise - замена scala.reflect, но умеет только синтаксические макро аннотации
  • Scalameta >= 2.0.0 - нацелен исключительно на тулзы

И как быть?

  • Если хватает макроаннотаций: Scalameta < 2.0.0 + macro-paradise, а потом переехать на scalamacros.
  • В остальных случаях: использовать scala.reflect и страдать, либо искать другие решения и ждать новых макросов.
  • Если можно решить проблему со стороны, написав тулзу или плагин к системе сборки: Scalameta >= 2.0.0

Итоги

  • Метапрограммирование со Scalameta - это просто
  • Инструменты развиваются и переходят из экспериментального статуса в стабильный, но переход еще в процессе
  • Макроаннотации - реально полезная вещь, которую уже можно использовать
  • С макросами лучше пока подождать

Полезные ссылки

Спасибо!

Метапрограммирование на Scala со scala.meta

By Yury Badalyants

Метапрограммирование на Scala со scala.meta

  • 722