¿Cómo funciona el tipo Dynamic work y cómo usarlo?

Escuché que con Dynamic es de alguna manera posible hacer tipeo dynamic en Scala. Pero no me puedo imaginar cómo podría ser eso o cómo funciona.

Descubrí que uno puede heredar del rasgo Dynamic

 class DynImpl extends Dynamic 

La API dice que uno puede usarlo así:

foo.method (“blah”) ~~> foo.applyDynamic (“método”) (“blah”)

Pero cuando lo pruebo no funciona:

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

Esto es completamente lógico, porque después de consultar las fonts , resultó que este rasgo está completamente vacío. No hay un método definido para aplicar applyDynamic y no me puedo imaginar cómo implementarlo por mi cuenta.

¿Puede alguien mostrarme lo que debo hacer para que funcione?

Scalas type Dynamic permite llamar a métodos en objetos que no existen o, en otras palabras, es una réplica de “método faltante” en los lenguajes dynamics.

Es correcto, scala.Dynamic no tiene ningún miembro, es solo una interfaz de marcador: el comstackdor rellena la implementación concreta. En cuanto a la característica de interpolación de cadenas de escalas , existen reglas bien definidas que describen la implementación generada. De hecho, uno puede implementar cuatro métodos diferentes:

  • selectDynamic – permite escribir selectDynamic de campo: foo.bar
  • updateDynamic – permite escribir actualizaciones de campo: foo.bar = 0
  • applyDynamic – permite llamar a métodos con argumentos: foo.bar(0)
  • applyDynamicNamed – permite llamar a métodos con argumentos nombrados: foo.bar(f = 0)

Para utilizar uno de estos métodos, basta con escribir una clase que amplíe Dynamic e implementar los métodos allí:

 class DynImpl extends Dynamic { // method implementations here } 

Además, es necesario agregar un

 import scala.language.dynamics 

o establezca la opción del comstackdor -language:dynamics porque la característica está oculta por defecto.

selectDynamic

selectDynamic es el más fácil de implementar. El comstackdor traduce una llamada de foo.bar a foo.selectDynamic("bar") , por lo que se requiere que este método tenga una lista de argumentos esperando una 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 puede ver, también es posible llamar explícitamente a los métodos dynamics.

updateDynamic

Debido a que updateDynamic se usa para actualizar un valor, este método debe devolver la Unit . Además, el comstackdor pasa el nombre del campo a actualizar y su valor a diferentes listas de argumentos:

 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 

El código funciona como se esperaba: es posible agregar métodos en tiempo de ejecución al código. Por otro lado, el código ya no es seguro y si se llama a un método que no existe, esto también se debe manejar en el tiempo de ejecución. Además, este código no es tan útil como en los lenguajes dynamics porque no es posible crear los métodos que deberían invocarse en el tiempo de ejecución. Esto significa que no podemos hacer algo como

 val name = "foo" d.$name 

donde d.$name se transformará en d.foo en el tiempo de ejecución. Pero esto no es tan malo porque incluso en lenguajes dynamics, esta es una característica peligrosa.

Otra cosa a tener en cuenta aquí es que updateDynamic debe implementarse junto con selectDynamic . Si no lo hacemos, obtendremos un error de comstackción: esta regla es similar a la implementación de un Setter, que solo funciona si hay un Getter con el mismo nombre.

aplicarDinámico

La capacidad de invocar métodos con argumentos la proporciona 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 :19: error: value selectDynamic is not a member of DynImpl 

El nombre del método y sus argumentos nuevamente están separados para diferentes listas de parámetros. Podemos llamar a métodos arbitrarios con un número arbitrario de argumentos, si queremos, pero si queremos llamar a un método sin paréntesis, debemos implementar selectDynamic .

Sugerencia: también es posible utilizar la syntax de la aplicación con applyDynamic :

 scala> d(5) res1: String = method 'apply' called with arguments '5' 

applyDynamicNamed

El último método disponible nos permite nombrar nuestros argumentos si queremos:

 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)' 

La diferencia en la firma del método es que applyDynamicNamed espera tuplas de la forma (String, A) donde A es un tipo arbitrario.


Todos los métodos anteriores tienen en común que sus parámetros se pueden parametrizar:

 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 

Afortunadamente, también es posible agregar argumentos implícitos: si agregamos un contexto de TypeTag enlazado, podemos verificar fácilmente los tipos de argumentos. Y lo mejor es que incluso el tipo de devolución es correcto, aunque tuvimos que agregar algunos moldes.

Pero Scala no sería Scala cuando no hay forma de encontrar una forma de evitar tales defectos. En nuestro caso, podemos usar clases de tipos para evitar los lanzamientos:

 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: _*) } } 

Si bien la implementación no se ve tan bien, su poder no puede cuestionarse:

 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 

En la parte superior de todo, también es posible combinar Dynamic con 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") :11: error: method 'noexist' with args 'a', 'b', 'c' doesn't exist d.noexist("a", "b", "c") ^ 

Las macros nos devuelven todas las garantías de tiempo de comstackción y aunque no es tan útil en el caso anterior, tal vez pueda ser muy útil para algunas DSL de Scala.

Si desea obtener aún más información sobre Dynamic hay algunos recursos más:

  • La propuesta oficial de SIP que introdujo Dynamic en Scala
  • Usos prácticos de un tipo dynamic en Scala : otra pregunta sobre SO (pero muy desactualizada)