Метапрограммирование на 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 - нацелен исключительно на тулзы
- https://github.com/scalacenter/macros - обещают сделать к марту 2018, но верится с трудом
И как быть?
- Если хватает макроаннотаций: Scalameta < 2.0.0 + macro-paradise, а потом переехать на scalamacros.
- В остальных случаях: использовать scala.reflect и страдать, либо искать другие решения и ждать новых макросов.
- Если можно решить проблему со стороны, написав тулзу или плагин к системе сборки: Scalameta >= 2.0.0
Итоги
- Метапрограммирование со Scalameta - это просто
- Инструменты развиваются и переходят из экспериментального статуса в стабильный, но переход еще в процессе
- Макроаннотации - реально полезная вещь, которую уже можно использовать
- С макросами лучше пока подождать
Полезные ссылки
- Quasiquotes cheetsheet: https://github.com/scalameta/scalameta/blob/master/notes/quasiquotes.md
- Scalameta gitter channel: https://github.com/scalameta/scalameta
- Scalameta tutorial: http://scalameta.org/tutorial
- Macro paradise tutorial: http://scalameta.org/paradise/
- AST explorer online: https://astexplorer.net/
Спасибо!
Метапрограммирование на Scala со scala.meta
By Yury Badalyants
Метапрограммирование на Scala со scala.meta
- 722