¿Cómo funciona Type Dynamic y cómo usarlo?

95

Escuché que con Dynamicél de alguna manera es posible escribir dinámicamente en Scala. Pero no puedo imaginar cómo se vería eso o cómo funciona.

Descubrí que se puede heredar de un rasgo Dynamic

class DynImpl extends Dynamic

La API dice que se puede usar así:

foo.method ("bla") ~~> foo.applyDynamic ("método") ("bla")

Pero cuando lo pruebo no 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")
               ^

Esto es completamente lógico, porque después de buscar las fuentes , resultó que este rasgo está completamente vacío. No hay un método applyDynamicdefinido y no puedo imaginar cómo implementarlo yo mismo.

¿Alguien puede mostrarme lo que necesito hacer para que funcione?

kiritsuku
fuente

Respuestas:

188

El tipo Scalas le Dynamicpermite llamar a métodos en objetos que no existen o, en otras palabras, es una réplica del "método perdido" en lenguajes dinámicos.

Es correcto, scala.Dynamicno tiene miembros, es solo una interfaz de marcador: la implementación concreta la completa el compilador. En cuanto a la función de interpolación de cadenas de Scalas, hay reglas bien definidas que describen la implementación generada. De hecho, se pueden implementar cuatro métodos diferentes:

  • selectDynamic - permite escribir accesos 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 con nombre: foo.bar(f = 0)

Para usar uno de estos métodos es suficiente escribir una clase que se extienda Dynamice 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 compilador -language:dynamicsporque la función está oculta de forma predeterminada.

selectDynamic

selectDynamices el más fácil de implementar. El compilador traduce una llamada de foo.bara foo.selectDynamic("bar"), por lo que se requiere que este método tenga una lista de argumentos que espera un 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 dinámicos.

updateDynamic

Debido a que updateDynamicse usa para actualizar un valor, este método debe regresar Unit. Además, el compilador 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 para los tipos y si se llama a un método que no existe, esto también debe manejarse en tiempo de ejecución. Además, este código no es tan útil como en los lenguajes dinámicos porque no es posible crear los métodos que se deben llamar en tiempo de ejecución. Esto significa que no podemos hacer algo como

val name = "foo"
d.$name

donde d.$namese transformaría ad.foo en tiempo de ejecución. Pero esto no es tan malo porque incluso en lenguajes dinámicos es una característica peligrosa.

Otra cosa a tener en cuenta aquí es que updateDynamicdebe implementarse junto conselectDynamic . Si no hacemos esto, obtendremos un error de compilación; esta regla es similar a la implementación de un Setter, que solo funciona si hay un Getter con el mismo nombre.

applyDynamic

La capacidad de llamar a métodos con argumentos es proporcionada 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

El nombre del método y sus argumentos nuevamente se separan en 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 implementarlo selectDynamic.

Sugerencia: también es posible utilizar la sintaxis de 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 applyDynamicNamedespera tuplas de la forma (String, A)donde Aes 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 TypeTaglímite de contexto, podemos verificar fácilmente los tipos de argumentos. Y lo mejor es que incluso el tipo de retorno es correcto, aunque tuvimos que agregar algunos moldes.

Pero Scala no sería Scala cuando no hay forma de evitar tales fallas. En nuestro caso, podemos usar clases de tipos para evitar los 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: _*)
  }

}

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

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 Dynamiccon 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")
                       ^

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

Si desea obtener aún más información, Dynamicexisten algunos recursos más:

kiritsuku
fuente
1
Definitivamente una gran respuesta y un escaparate de Scala Power
Herrington Darkholme
No lo llamaría poder en caso de que la función esté oculta de forma predeterminada, por ejemplo, podría ser experimental o no funcionar bien con otros, ¿o no?
matanster
¿Hay información sobre el rendimiento de Scala Dynamic? Sé que Scala Reflection es lento (así viene Scala-macro). ¿El uso de Scala Dynamic ralentizará drásticamente el rendimiento?
windweller
1
@AllenNie Como puede ver en mi respuesta, hay diferentes formas de implementarlo. Si usa macros, ya no hay sobrecarga, ya que la llamada dinámica se resuelve en tiempo de compilación. Si usa do checks en tiempo de ejecución, debe realizar la verificación de parámetros para enviar correctamente a la ruta de código correcta. Eso no debería ser más sobrecarga que cualquier otra verificación de parámetros en su aplicación. Si hace uso de la reflexión, obviamente obtendrá más sobrecarga, pero debe medir usted mismo cuánto ralentiza su aplicación.
kiritsuku
1
"Las macros nos devuelven todas las garantías de tiempo de compilación" - esto me está
volviendo