Scala traduz a seguinte chamada de atribuição em chamadas para o método de atualização.
Com a seguinte assinatura de atualização,
def update(a: Int, b: Int, c: Int, value: Boolean): Unit
Um é capaz de escrever
obj(1, 2, 3) = true
e é traduzido para
obj.update(1, 2, 3, true)
Isso tudo é ótimo. No entanto, há uma desvantagem.
E se eu quiser que a lista de parâmetros no açúcar sintático seja um vararg?
obj(1, 2, 3) = true
obj(2, 3, 4, 5) = false
A maneira ideal de apoiar isso é permitir currying no método de atualização
def update(dims: Int*)(value: Boolean): Unit
Más notícias, o scala não suporta isso.
Boas notícias, Macro para o resgate!
Design de interface
Os programadores são preguiçosos.
Portanto, apenas adicionamos uma anotação à definição do método de atualização.
@CurriedUpdate
def update[T](a: Int, b: Int, cs: Int*)(value: T): Unit = ???
Implementação
A definição do método é primeiro renomeada para outro nome fixo e acrescentamos um outro método delegado, que corresponde exatamente à assinatura necessária ao compilador Scala para expandir o açúcar sintático.
Que tal os tipos? Não podemos expressar os mesmos tipos em um método de atualização sem curry, porque se pudermos, não teremos esse problema.
Uma maneira possível e de alguma forma brutal é definir os parâmetros como Any * e, em seguida, passar na verificação de tipo, usar muito asInstanceOf para lançar os params.
Parece funcionar, certo? Podemos fazer melhor.
Delegar macro
Definimos essa implementação de delegação como outra macro, então os tipos não importam!
def updateDelegate(c: Context)(args: c.Expr[Any]*): c.Expr[Any] = ???
Vamos primeiro escrever essa macro!
Ele deve despachar o args
para o método de atualização original renomeado e reordenado. Lembra que renomeamos o método anotado original com outro nome? Sim, aquele. (O ideal é que haja uma maneira melhor, se o sistema de macro do Scala permitir passar parâmetros estáticos para a implementação da macro, de modo que não seja necessário renomear para um nome fixo )
Dividimos os valores em duas partes – a última (que é o valor alvo) e as outras, e chamamos o método original com lista de dois argumentos. Simples o suficiente!
def updateDelegate(c: Context)(args: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
c.Expr[Unit] {
Apply(
Apply(
Select(c.prefix.tree, TermName(updateRenamed)),
values.take(values.size - 1).map(_.tree).toList
),
List(values.last.tree)
)
}
}
Anotação de macro
Agora vamos escrever a macro que chama a macro delegada!
A parte principal – a árvore resultante, é bastante fácil,
c.Expr[Any] {
q"""
import scala.language.experimental.macros
${renameMethod(methodDef, updateRenamed)}
def update(values: Any*): Unit = macro CurriedUpdate.updateMacroDispatcher
"""
}
renameMethod
é um método utilitário que renomeia um DefDef (nó AST de definição de método).
Resumo
Basicamente, estamos prontos para ir!
Para finalizar,
import language.experimental.macros
import scala.reflect.macros.blackbox.Context
import scala.annotation.StaticAnnotation
class CurriedUpdate extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro CurriedUpdate.curry
}
object CurriedUpdate {
private val updateRenamed = "updateR"
def curry(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
def renameMethod(method: DefDef, newName: String): DefDef = {
DefDef(
method.mods, TermName(newName), method.tparams,
method.vparamss,
method.tpt, method.rhs)
}
val methodDef = annottees(0).tree match {
case m: DefDef => m
case _ => c.abort(c.enclosingPosition, "CurriedUpdate can only be applied on method")
}
if (methodDef.name.decodedName.toString != "update") {
c.abort(c.enclosingPosition, "CurriedUpdate can only be applied to the update method")
}
if (methodDef.vparamss.size != 2) {
c.abort(c.enclosingPosition, "Curried update must have two argument list")
}
if (methodDef.vparamss(0).size == 0) {
c.abort(c.enclosingPosition, "The first argument list must not be empty")
}
if (methodDef.vparamss(1).size != 1) {
c.abort(c.enclosingPosition, "The second argument list must have only one element")
}
c.Expr[Any] {
q"""
import scala.language.experimental.macros
${renameMethod(methodDef, updateRenamed)}
def update(values: Any*): Unit = macro CurriedUpdate.updateMacroDispatcher
"""
}
}
def updateMacroDispatcher(c: Context)(values: c.Expr[Any]*): c.Expr[Unit] = {
import c.universe._
c.Expr[Unit] {
Apply(
Apply(
Select(c.prefix.tree, TermName(updateRenamed)),
values.take(values.size - 1).map(_.tree).toList
),
List(values.last.tree)
)
}
}
}
O código extra é para verificação da forma do alvo da anotação.
É hora de testar.
scala> :pa
// Entering paste mode (ctrl-D to finish)
class Test {
@CurriedUpdate
def update(a: Int, b: Int, cs: Int*)(value: Boolean) = {
println(s"update $a $b ${cs mkString " "} to $value")
}
}
defined class Test
scala> val t = new Test
t: Test = Test@3f2eb247
scala> t(1, 2, 3, 4) = false
update 1 2 3 4 to false
scala> t(1) = false
<console>:10: error: not enough arguments for method updateR: (a: Int, b: Int, c
s: Int*)(value: Boolean)Unit.
Unspecified value parameters b, cs.
t(1) = false
^
scala> t(1, 2) = true
update 1 2 to true
Parece funcionar! Ei!