Макро-аннотация для переопределения toString функции Scala
Как написать аннотацию макроса, которая выглядит как использование @named("+2") _ + 2
и производит:
new (Int => Int) {
override def toString(): String = "+2"
def apply(x: Int): Int = x + 2
}
1 ответ
Вы можете создать макрос, который возвращает анонимную функцию. Вы не получаете полностью нужный синтаксис, кажется, что @ не работает внутри методов.
import scala.language.experimental.macros
import scala.reflect.macros._
object Named {
def build[T, R](name: String)(applyFunc: T => R): T => R = macro Named.impl[T, R]
def impl[T: c.WeakTypeTag, R: c.WeakTypeTag](c: whitebox.Context)(name: c.Expr[String])(applyFunc: c.Expr[T => R]): c.Expr[T => R] = {
import c.universe._
val functionType = weakTypeOf[T]
val resultType = weakTypeOf[R]
c.Expr[T => R](
c.typecheck(q"""
new ($functionType => $resultType) {
override def toString() = $name
def apply(x: $functionType): $resultType = $applyFunc(x)
}
"""))
}
}
а затем используйте этот макрос для генерации вашей собственной функции:
class NamedTest {
@Test
def testNamed() = {
val b = Named.build[Int, Int]("+2")(_ + 2)
assertEquals(4, b(2))
assertEquals("+2", b.toString)
}
}
Правильный синтаксис ((_: Int) + 2): @named("+2")
. К сожалению, аннотации макросов, аннотирующие выражения, не расширяются.
Самый простой - использовать
object Named {
def build[T, R](name: String)(applyFunc: T => R): T => R = new (T => R) {
override def toString() = name
def apply(x: T): R = applyFunc(x)
}
}
без макросов.
В противном случае Scalameta может расширять аннотации к выражениям:
build.sbt (документация sbt по генерации исходников находится здесь)
ThisBuild / name := "scalametademo"
lazy val commonSettings = Seq(
scalaVersion := "2.13.1",
)
lazy val annotations = project
.settings(
commonSettings,
)
lazy val helpers = project
.settings(
commonSettings,
)
lazy val in = project
.dependsOn(annotations)
.settings(
commonSettings,
)
lazy val out = project
.dependsOn(helpers)
.settings(
sourceGenerators in Compile += Def.task {
Generator.gen(
inputDir = sourceDirectory.in(in, Compile).value,
outputDir = sourceManaged.in(Compile).value
)
}.taskValue,
commonSettings,
)
проект /build.sbt
libraryDependencies += "org.scalameta" %% "scalameta" % "4.3.0"
проект /Generator.scala
import sbt._
object Generator {
def gen(inputDir: File, outputDir: File): Seq[File] = {
val finder: PathFinder = inputDir ** "*.scala"
for(inputFile <- finder.get) yield {
val inputStr = IO.read(inputFile)
val outputFile = outputDir / inputFile.toURI.toString.stripPrefix(inputDir.toURI.toString)
val outputStr = Transformer.transform(inputStr)
IO.write(outputFile, outputStr)
outputFile
}
}
}
проект /Transformer.scala
import scala.meta._
object Transformer {
val getNamedAnnotationParam: PartialFunction[Mod, Lit] = {
case mod"@named(...${List(List(s: Lit))})" => s
}
val isNamedAnnotated: Mod => Boolean = getNamedAnnotationParam.lift(_).isDefined
def transform(input: String): String = transform(input.parse[Source].get).toString
def transform(input: Tree): Tree = input.transform {
case q"package $eref { ..$stats }" =>
val stats1 = stats.filter {
case q"import ..${List(importer"annotations.{..$importeesnel}")}" => false
case _ => true
}
q"package $eref { ..$stats1 }"
case q"$expr: ..@$annotsnel" if annotsnel.exists(isNamedAnnotated) =>
val annotsnel1 = annotsnel.filterNot(isNamedAnnotated)
val name = annotsnel.collect(getNamedAnnotationParam).head
val expr1 = expr match {
case q"(..$params) => $expr2" =>
val params1 = params.map {
case param"..$mods $name: ${Some(tpe)} = $expropt" =>
param"..$mods $name: ${Some(tpe)} = $expropt"
case param"..$mods $name: ${None} = $expropt" =>
param"..$mods $name: scala.Any = $expropt"
}
val domain = params1.map {
case param"..$mods $name: $tpeopt = $expropt" => tpeopt.get
}
q"""
val typed = com.example.helpers.${Term.Name("TypedFunction" + params.length)}($expr)
new ((..$domain) => typed.CoDomain) {
override def toString(): String = $name
def apply(..$params1): typed.CoDomain = $expr2
}
"""
case e => e
}
if (annotsnel1.nonEmpty)
q"$expr1: ..@$annotsnel1"
else q"$expr1"
}
}
аннотации / src / main / scala / com / example / annotations / named.scala
package com.example.annotations
import scala.annotation.StaticAnnotation
class named(name: String) extends StaticAnnotation
помощники /src/main/scala/com/example/helpers/TypedFunctions.scala
package com.example.helpers
sealed trait TypedFunctions[_CoDomain] {
type CoDomain = _CoDomain
}
case class TypedFunction0[_CoDomain](f: () => _CoDomain) extends TypedFunctions[_CoDomain]
case class TypedFunction1[_Domain, _CoDomain](f: _Domain => _CoDomain) extends TypedFunctions[_CoDomain]
case class TypedFunction2[_Domain1, _Domain2, _CoDomain](f: (_Domain1, _Domain2) => _CoDomain) extends TypedFunctions[_CoDomain]
case class TypedFunction3[_Domain1, _Domain2, _Domain3, _CoDomain](f: (_Domain1, _Domain2, _Domain3) => _CoDomain) extends TypedFunctions[_CoDomain]
в / src / main / scala / com / example / App.scala
package com.example
import annotations.named
object App {
(((x: Int) => x + 2): @named("+2"))
(((x: Int, y: Int) => x + y): @named("+"))
}
out / target / scala-2.13 / src_managed / main / scala / com / example / App.scala (послеsbt "; project out; clean; compile"
)
package com.example
object App {
{
val typed = com.example.helpers.TypedFunction1 { (x: Int) => x + 2 }
new (Int => typed.CoDomain) {
override def toString(): String = "+2"
def apply(x: Int): typed.CoDomain = x + 2
}
}
{
val typed = com.example.helpers.TypedFunction2 { (x: Int, y: Int) => x + y }
new ((Int, Int) => typed.CoDomain) {
override def toString(): String = "+"
def apply(x: Int, y: Int): typed.CoDomain = x + y
}
}
}
Другой пример: как объединить несколько операций импорта в scala?