Le type Scalas Dynamic
vous 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.Dynamic
n'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 Dynamic
et 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:dynamics
car la fonctionnalité est masquée par défaut.
selectDynamique
selectDynamic
est le plus simple à mettre en œuvre. Le compilateur traduit un appel de foo.bar
to 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 updateDynamic
est 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.$name
serait transformé en d.foo
au 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 updateDynamic
doit ê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 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 également possible d'ajouter des arguments implicites - si nous ajoutons un TypeTag
contexte 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 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 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, Dynamic
il existe d'autres ressources: