Suporte de atualização Varargs no Scala

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 argspara 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!