Type de Scalas Dynamic
vous permet d'appeler des méthodes sur des objets qui n'existent pas ou, en d'autres termes, il s'agit d'une réplique de la "méthode manquante" dans les langages dynamiques.
C'est correct, scala.Dynamic
n'a pas de membres, c'est juste une interface de marquage - l'implémentation concrète est remplie par le compilateur. Quant aux Scalas Interpolation des chaînes de caractères 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 champs : foo.bar
-
updateDynamic
- permet d'écrire des mises à jour de champs : 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 une de ces méthodes, il suffit d'écrire une classe qui prolonge Dynamic
et d'y mettre en œuvre les méthodes :
class DynImpl extends Dynamic {
// method implementations here
}
En outre, il faut ajouter un
import scala.language.dynamics
ou définir l'option de compilation -language:dynamics
car la fonction est masquée par défaut.
selectDynamic
selectDynamic
est le plus facile à mettre en œuvre. Le compilateur traduit un appel de foo.bar
a foo.selectDynamic("bar")
Il est donc nécessaire que cette méthode ait une liste d'arguments qui attende un numéro d'identification de la méthode. 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 les méthodes dynamiques de manière explicite.
updateDynamic
Parce que updateDynamic
est utilisé pour mettre à jour une valeur, cette méthode doit retourner Unit
. De plus, le nom du champ à mettre à jour et sa valeur sont passés dans des listes d'arguments différentes 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 code au moment de l'exécution. D'un autre côté, le code n'est plus typiquement sûr et si une méthode qui n'existe pas est appelée, cela doit être géré à l'exécution également. 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 à l'exécution. Cela signifie que nous ne pouvons pas faire quelque chose comme
val name = "foo"
d.$name
donde d.$name
serait transformé en d.foo
au moment de l'exécution. Mais ce n'est pas si grave, car même dans les langages dynamiques, cette fonctionnalité est dangereuse.
Une autre chose à noter ici, c'est que updateDynamic
doit être mis en œuvre conjointement 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 portant le même nom.
applyDynamic
La possibilité d'appeler des méthodes avec des arguments est fournie par l'outil 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 dans des listes de paramètres différentes. Nous pouvons appeler des méthodes arbitraires avec un nombre arbitraire d'arguments si nous le souhaitons, mais si nous voulons appeler une méthode sans aucune parenthèse, nous devons implémenter la méthode suivante selectDynamic
.
Conseil : 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 souhaitons :
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 que applyDynamicNamed
attend des tuples de la forme (String, A)
où A
est 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 aussi possible d'ajouter des arguments implicites - si nous ajoutons un TypeTag
nous pouvons facilement vérifier les types d'arguments. Et la meilleure chose est que même le type de retour est correct - même si nous avons dû ajouter quelques casts.
Mais Scala ne serait pas Scala s'il n'y a pas moyen de trouver un moyen de contourner de telles failles. Dans notre cas, nous pouvons utiliser des classes de type pour éviter les castings :
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 la mise en œuvre ne soit pas très jolie, 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
Au sommet de tout, il est également possible de combiner Dynamic
avec 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 rendent toutes les garanties de compilation et, bien que cela ne soit pas très utile dans le cas ci-dessus, cela peut être très utile pour certains DSL Scala.
Si vous voulez obtenir encore plus d'informations sur Dynamic
il existe d'autres ressources :