El tipo Scalas le Dynamic
permite 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.Dynamic
no 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 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 compilador -language:dynamics
porque la función está oculta de forma predeterminada.
selectDynamic
selectDynamic
es el más fácil de implementar. El compilador traduce una llamada de foo.bar
a 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 updateDynamic
se 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.$name
se 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 updateDynamic
debe 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 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 TypeTag
lí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 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")
<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, Dynamic
existen algunos recursos más: