O tipo Scalas Dynamic
permite que você chame métodos em objetos que não existem ou, em outras palavras, é uma réplica de "método ausente" em linguagens dinâmicas.
Está correto, scala.Dynamic
não possui membros, é apenas uma interface de marcador - a implementação concreta é preenchida pelo compilador. Quanto ao recurso Scalas String Interpolation , existem regras bem definidas que descrevem a implementação gerada. Na verdade, pode-se implementar quatro métodos diferentes:
selectDynamic
- permite escrever acessadores de campo: foo.bar
updateDynamic
- permite escrever atualizações de campo: foo.bar = 0
applyDynamic
- permite chamar métodos com argumentos: foo.bar(0)
applyDynamicNamed
- permite chamar métodos com argumentos nomeados: foo.bar(f = 0)
Para usar um desses métodos, basta escrever uma classe que estenda Dynamic
e implementar os métodos lá:
class DynImpl extends Dynamic {
// method implementations here
}
Além disso, é necessário adicionar um
import scala.language.dynamics
ou defina a opção do compilador -language:dynamics
porque o recurso está oculto por padrão.
selectDynamic
selectDynamic
é o mais fácil de implementar. O compilador traduz uma chamada de foo.bar
para foo.selectDynamic("bar")
, portanto, é necessário que esse método tenha uma lista de argumentos esperando um String
:
class DynImpl extends Dynamic {
def selectDynamic(name: String) = name
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@6040af64
scala> d.foo
res37: String = foo
scala> d.bar
res38: String = bar
scala> d.selectDynamic("foo")
res54: String = foo
Como se pode ver, também é possível chamar os métodos dinâmicos explicitamente.
updateDynamic
Como updateDynamic
é usado para atualizar um valor, esse método precisa retornar Unit
. Além disso, o nome do campo a ser atualizado e seu valor são passados para diferentes listas de argumentos pelo compilador:
class DynImpl extends Dynamic {
var map = Map.empty[String, Any]
def selectDynamic(name: String) =
map get name getOrElse sys.error("method not found")
def updateDynamic(name: String)(value: Any) {
map += name -> value
}
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@7711a38f
scala> d.foo
java.lang.RuntimeException: method not found
scala> d.foo = 10
d.foo: Any = 10
scala> d.foo
res56: Any = 10
O código funciona conforme o esperado - é possível adicionar métodos em tempo de execução ao código. Por outro lado, o código não é mais à prova de digitação e se um método que não existe for chamado, ele deve ser tratado em tempo de execução também. Além disso, esse código não é tão útil quanto em linguagens dinâmicas, pois não é possível criar os métodos que devem ser chamados em tempo de execução. Isso significa que não podemos fazer algo como
val name = "foo"
d.$name
onde d.$name
seria transformado d.foo
em tempo de execução. Mas isso não é tão ruim porque, mesmo em linguagens dinâmicas, esse é um recurso perigoso.
Outra coisa a notar aqui é que updateDynamic
precisa ser implementado junto comselectDynamic
. Se não fizermos isso, obteremos um erro de compilação - esta regra é semelhante à implementação de um Setter, que só funciona se houver um Getter com o mesmo nome.
applyDynamic
A capacidade de chamar métodos com argumentos é fornecida por applyDynamic
:
class DynImpl extends Dynamic {
def applyDynamic(name: String)(args: Any*) =
s"method '$name' called with arguments ${args.mkString("'", "', '", "'")}"
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@766bd19d
scala> d.ints(1, 2, 3)
res68: String = method 'ints' called with arguments '1', '2', '3'
scala> d.foo()
res69: String = method 'foo' called with arguments ''
scala> d.foo
<console>:19: error: value selectDynamic is not a member of DynImpl
O nome do método e seus argumentos são novamente separados em listas de parâmetros diferentes. Podemos chamar métodos arbitrários com um número arbitrário de argumentos se quisermos, mas se quisermos chamar um método sem parênteses, precisamos implementar selectDynamic
.
Dica: também é possível usar a sintaxe de aplicação com applyDynamic
:
scala> d(5)
res1: String = method 'apply' called with arguments '5'
applyDynamicNamed
O último método disponível nos permite nomear nossos argumentos se quisermos:
class DynImpl extends Dynamic {
def applyDynamicNamed(name: String)(args: (String, Any)*) =
s"method '$name' called with arguments ${args.mkString("'", "', '", "'")}"
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@123810d1
scala> d.ints(i1 = 1, i2 = 2, 3)
res73: String = method 'ints' called with arguments '(i1,1)', '(i2,2)', '(,3)'
A diferença na assinatura do método é que applyDynamicNamed
espera tuplas da forma em (String, A)
que A
é um tipo arbitrário.
Todos os métodos acima têm em comum que seus parâmetros podem ser parametrizados:
class DynImpl extends Dynamic {
import reflect.runtime.universe._
def applyDynamic[A : TypeTag](name: String)(args: A*): A = name match {
case "sum" if typeOf[A] =:= typeOf[Int] =>
args.asInstanceOf[Seq[Int]].sum.asInstanceOf[A]
case "concat" if typeOf[A] =:= typeOf[String] =>
args.mkString.asInstanceOf[A]
}
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@5d98e533
scala> d.sum(1, 2, 3)
res0: Int = 6
scala> d.concat("a", "b", "c")
res1: String = abc
Felizmente, também é possível adicionar argumentos implícitos - se adicionarmos um TypeTag
limite de contexto, podemos verificar facilmente os tipos dos argumentos. E o melhor é que até o tipo de retorno está correto - embora tenhamos que adicionar alguns casts.
Mas Scala não seria Scala quando não há como encontrar uma maneira de contornar essas falhas. No nosso caso, podemos usar classes de tipo para evitar os casts:
object DynTypes {
sealed abstract class DynType[A] {
def exec(as: A*): A
}
implicit object SumType extends DynType[Int] {
def exec(as: Int*): Int = as.sum
}
implicit object ConcatType extends DynType[String] {
def exec(as: String*): String = as.mkString
}
}
class DynImpl extends Dynamic {
import reflect.runtime.universe._
import DynTypes._
def applyDynamic[A : TypeTag : DynType](name: String)(args: A*): A = name match {
case "sum" if typeOf[A] =:= typeOf[Int] =>
implicitly[DynType[A]].exec(args: _*)
case "concat" if typeOf[A] =:= typeOf[String] =>
implicitly[DynType[A]].exec(args: _*)
}
}
Embora a implementação não pareça tão boa, seu poder não pode ser questionado:
scala> val d = new DynImpl
d: DynImpl = DynImpl@24a519a2
scala> d.sum(1, 2, 3)
res89: Int = 6
scala> d.concat("a", "b", "c")
res90: String = abc
Acima de tudo, também é possível combinar Dynamic
com macros:
class DynImpl extends Dynamic {
import language.experimental.macros
def applyDynamic[A](name: String)(args: A*): A = macro DynImpl.applyDynamic[A]
}
object DynImpl {
import reflect.macros.Context
import DynTypes._
def applyDynamic[A : c.WeakTypeTag](c: Context)(name: c.Expr[String])(args: c.Expr[A]*) = {
import c.universe._
val Literal(Constant(defName: String)) = name.tree
val res = defName match {
case "sum" if weakTypeOf[A] =:= weakTypeOf[Int] =>
val seq = args map(_.tree) map { case Literal(Constant(c: Int)) => c }
implicitly[DynType[Int]].exec(seq: _*)
case "concat" if weakTypeOf[A] =:= weakTypeOf[String] =>
val seq = args map(_.tree) map { case Literal(Constant(c: String)) => c }
implicitly[DynType[String]].exec(seq: _*)
case _ =>
val seq = args map(_.tree) map { case Literal(Constant(c)) => c }
c.abort(c.enclosingPosition, s"method '$defName' with args ${seq.mkString("'", "', '", "'")} doesn't exist")
}
c.Expr(Literal(Constant(res)))
}
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@c487600
scala> d.sum(1, 2, 3)
res0: Int = 6
scala> d.concat("a", "b", "c")
res1: String = abc
scala> d.noexist("a", "b", "c")
<console>:11: error: method 'noexist' with args 'a', 'b', 'c' doesn't exist
d.noexist("a", "b", "c")
^
As macros nos devolvem todas as garantias de tempo de compilação e, embora não seja tão útil no caso acima, talvez possa ser muito útil para algumas DSLs Scala.
Se você deseja obter ainda mais informações sobre, Dynamic
existem mais alguns recursos: