102 votes

Comment fonctionne le type Dynamic et comment l'utiliser ?

J'ai entendu dire qu'avec Dynamic il est en quelque sorte possible de faire du typage dynamique en Scala. Mais je n'arrive pas à imaginer à quoi cela pourrait ressembler ou comment cela fonctionne.

J'ai découvert qu'on peut hériter d'un trait de caractère Dynamic

class DynImpl extends Dynamic

El API dit qu'on peut l'utiliser comme ça :

foo.method("blah") ~~> foo.applyDynamic("method")("blah")

Mais quand je l'essaie, ça ne marche pas :

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

C'est tout à fait logique, parce qu'après avoir regardé à la sources il s'est avéré que ce trait est complètement vide. Il n'y a pas de méthode applyDynamic défini et je ne peux pas imaginer comment le mettre en œuvre par moi-même.

Quelqu'un peut-il me montrer ce que je dois faire pour que cela fonctionne ?

201voto

sschaef Points 20242

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)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 :

1 votes

Une excellente réponse et une vitrine de la puissance de Scala.

0 votes

Je n'appellerais pas ça puissance dans le cas où la fonctionnalité est cachée par défaut, par exemple parce qu'elle peut être expérimentale ou qu'elle n'est pas compatible avec d'autres, ou est-ce le cas ?

0 votes

Existe-t-il des informations sur les performances de Scala Dynamic ? Je sais que Scala Reflection est lent (d'où Scala-macro). L'utilisation de Scala Dynamic va-t-elle ralentir les performances de façon spectaculaire ?

0voto

Powers Points 1742

La réponse de Kiritsuku est meilleure. Il s'agit de montrer un cas d'utilisation pratique de type TL;DR.

La dynamique peut être utilisée pour construire dynamiquement un objet avec le modèle de construction.

import scala.language.dynamics

case class DynImpl(
  inputParams: Map[String, List[String]] = Map.empty[String, List[String]]
) extends Dynamic {

  def applyDynamic(name: String)(args: String*): DynImpl = {
    copy(inputParams = inputParams ++ Map(name -> args.toList))
  }

}

val d1 = DynImpl().whatever("aaa", "bbb").cool("ccc")
println(d1.inputParams) // Map(whatever -> List(aaa, bbb), cool -> List(ccc))

val d2 = DynImpl().whatever("aaa", "bbb").fun("haha")
println(d2.inputParams) // Map(whatever -> List(aaa, bbb), fun -> List(haha))

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X