Le type Scalas Dynamicvous permet d'appeler des méthodes sur des objets qui n'existent pas ou en d'autres termes c'est une réplique de "méthode manquante" dans les langages dynamiques.
C'est correct, scala.Dynamicn'a pas de membres, c'est juste une interface de marqueur - l'implémentation concrète est complétée par le compilateur. En ce qui concerne la fonction d' interpolation de chaîne Scalas, il existe des règles bien définies décrivant l'implémentation générée. En fait, on peut mettre en œuvre quatre méthodes différentes:
selectDynamic - permet d'écrire des accesseurs de champ: foo.bar
updateDynamic - permet d'écrire des mises à jour de champ: foo.bar = 0
applyDynamic - permet d'appeler des méthodes avec des arguments: foo.bar(0)
applyDynamicNamed - permet d'appeler des méthodes avec des arguments nommés: foo.bar(f = 0)
Pour utiliser l'une de ces méthodes, il suffit d'écrire une classe qui s'étend Dynamicet d'y implémenter les méthodes:
class DynImpl extends Dynamic {
// method implementations here
}
De plus, il faut ajouter un
import scala.language.dynamics
ou définissez l'option du compilateur -language:dynamicscar la fonctionnalité est masquée par défaut.
selectDynamique
selectDynamicest le plus simple à mettre en œuvre. Le compilateur traduit un appel de foo.barto foo.selectDynamic("bar"), il est donc nécessaire que cette méthode ait une liste d'arguments attendant 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
Comme on peut le voir, il est également possible d'appeler explicitement les méthodes dynamiques.
updateDynamic
Parce qu'elle updateDynamicest utilisée pour mettre à jour une valeur, cette méthode doit renvoyer Unit. De plus, le nom du champ à mettre à jour et sa valeur sont passés à différentes listes d'arguments par le compilateur:
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
Le code fonctionne comme prévu - il est possible d'ajouter des méthodes au moment de l'exécution au code. D'un autre côté, le code n'est plus de type sécurisé et si une méthode appelée qui n'existe pas, elle doit également être gérée au moment de l'exécution. De plus, ce code n'est pas aussi utile que dans les langages dynamiques car il n'est pas possible de créer les méthodes qui doivent être appelées au moment de l'exécution. Cela signifie que nous ne pouvons pas faire quelque chose comme
val name = "foo"
d.$name
où d.$nameserait transformé en d.fooau moment de l'exécution. Mais ce n'est pas si mal car même dans les langages dynamiques, c'est une fonctionnalité dangereuse.
Une autre chose à noter ici, c'est que cela updateDynamicdoit être mis en œuvre avec selectDynamic. Si nous ne le faisons pas, nous obtiendrons une erreur de compilation - cette règle est similaire à l'implémentation d'un Setter, qui ne fonctionne que s'il existe un Getter avec le même nom.
appliquerDynamique
La possibilité d'appeler des méthodes avec des arguments est fournie par 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
Le nom de la méthode et ses arguments sont à nouveau séparés en différentes listes de paramètres. Nous pouvons appeler des méthodes arbitraires avec un nombre arbitraire d'arguments si nous le voulons, mais si nous voulons appeler une méthode sans parenthèses, nous devons l'implémenter selectDynamic.
Astuce: Il est également possible d'utiliser apply-syntax avec applyDynamic:
scala> d(5)
res1: String = method 'apply' called with arguments '5'
applyDynamicNamed
La dernière méthode disponible nous permet de nommer nos arguments si nous le voulons:
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 différence dans la signature de la méthode est qu'elle applyDynamicNamedattend des tuples de la forme (String, A)où Aest un type arbitraire.
Toutes les méthodes ci-dessus ont en commun que leurs paramètres peuvent être paramétrés:
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
Heureusement, il est également possible d'ajouter des arguments implicites - si nous ajoutons un TypeTagcontexte lié, nous pouvons facilement vérifier les types des arguments. Et la meilleure chose est que même le type de retour est correct - même si nous avons dû ajouter quelques lancers.
Mais Scala ne serait pas Scala quand il n'y a aucun moyen de trouver un moyen de contourner ces défauts. Dans notre cas, nous pouvons utiliser des classes de types pour éviter les 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: _*)
}
}
Bien que l'implémentation ne soit pas si belle, sa puissance ne peut être remise en question:
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
Pour couronner le tout, il est également possible de combiner Dynamicavec des 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")
^
Les macros nous donnent toutes les garanties de temps de compilation et bien que ce ne soit pas très utile dans le cas ci-dessus, cela peut peut-être être très utile pour certains DSL Scala.
Si vous souhaitez obtenir encore plus d'informations, Dynamicil existe d'autres ressources: