Como funciona o tipo Dynamic e como usá-lo?

95

Ouvi dizer que Dynamic, de alguma forma, é possível fazer digitação dinâmica no Scala. Mas não consigo imaginar como isso pode ser ou como funciona.

Eu descobri que se pode herdar de traço Dynamic

class DynImpl extends Dynamic

A API diz que pode ser usado assim:

foo.method ("blah") ~~> foo.applyDynamic ("method") ("blah")

Mas quando eu tento, não funciona:

scala> (new DynImpl).method("blah")
<console>:17: error: value applyDynamic is not a member of DynImpl
error after rewriting to new DynImpl().<applyDynamic: error>("method")
possible cause: maybe a wrong Dynamic method signature?
              (new DynImpl).method("blah")
               ^

Isso é completamente lógico, porque depois de olhar as fontes , descobriu-se que esse traço está completamente vazio. Não existe um método applyDynamicdefinido e não consigo imaginar como implementá-lo sozinho.

Alguém pode me mostrar o que preciso fazer para que funcione?

Kiritsuku
fonte

Respostas:

188

O tipo Scalas Dynamicpermite 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.Dynamicnã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 Dynamice 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:dynamicsporque o recurso está oculto por padrão.

selectDynamic

selectDynamicé o mais fácil de implementar. O compilador traduz uma chamada de foo.barpara 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.$nameseria transformado d.fooem 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 updateDynamicprecisa 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 applyDynamicNamedespera 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 TypeTaglimite 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 Dynamiccom 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, Dynamicexistem mais alguns recursos:

Kiritsuku
fonte
1
Definitivamente, uma ótima resposta e uma demonstração de Scala Power
Herrington Darkholme
Eu não chamaria isso de potência caso o recurso esteja oculto por padrão, por exemplo, pode ser experimental ou não funcionar bem com os outros, ou não?
matanster
Existe alguma informação sobre o desempenho do Scala Dynamic? Eu sei que o Scala Reflection é lento (daí vem o Scala-macro). O uso do Scala Dynamic diminuirá drasticamente o desempenho?
Windweller
1
@AllenNie Como você pode ver na minha resposta, existem diferentes maneiras de implementá-lo. Se você usar macros, não haverá mais sobrecarga, pois a chamada dinâmica é resolvida em tempo de compilação. Se você usar do checks at runtime, terá que fazer a verificação de parâmetro para despachar corretamente para o caminho de código correto. Isso não deve ser mais sobrecarga do que qualquer outro parâmetro de verificação em seu aplicativo. Se você fizer uso da reflexão, obviamente obterá mais sobrecarga, mas terá que medir por si mesmo o quanto isso torna sua aplicação mais lenta.
Kiritsuku
1
"As macros nos devolvem todas as garantias de tempo de compilação" - isso está me
espantando