Comment fonctionne le type Dynamic et comment l'utiliser?

95

J'ai entendu dire qu'avec Dynamicil est en quelque sorte possible de faire une saisie dynamique dans Scala. Mais je ne peux pas imaginer à quoi cela pourrait ressembler ou comment cela fonctionne.

J'ai découvert que l'on peut hériter d'un trait Dynamic

class DynImpl extends Dynamic

L' API dit que l'on peut l'utiliser comme ceci:

toto.method ("bla") ~~> toto.applyDynamic ("méthode") ("bla")

Mais quand je l'essaye, ç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, car après avoir regardé les sources , il s'est avéré que ce trait est complètement vide. Il n'y a pas de méthode applyDynamicdéfinie et je ne peux pas imaginer comment la mettre en œuvre moi-même.

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

Kiritsuku
la source

Réponses:

188

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

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

Kiritsuku
la source
1
Certainement une excellente réponse et une vitrine de Scala Power
Herrington Darkholme
Je n'appellerais pas cela de la puissance au cas où la fonctionnalité est masquée par défaut, par exemple pourrait être expérimentale ou ne pas bien fonctionner avec les autres, ou est-ce que c'est?
matanster
Existe-t-il des informations sur les performances de Scala Dynamic? Je sais que la réflexion Scala est lente (vient donc Scala-macro). L'utilisation de Scala Dynamic ralentira-t-elle considérablement les performances?
windweller
1
@AllenNie Comme vous pouvez le voir dans ma réponse, il existe différentes manières de l'implémenter. Si vous utilisez des macros, il n'y a plus de surcharge, puisque l'appel dynamique est résolu au moment de la compilation. Si vous utilisez do checks lors de l'exécution, vous devez effectuer la vérification des paramètres afin de distribuer correctement au bon chemin de code. Cela ne devrait pas être plus de surcharge que tout autre contrôle de paramètre dans votre application. Si vous utilisez la réflexion, vous obtenez évidemment plus de frais généraux, mais vous devez mesurer vous-même à quel point cela ralentit votre application.
kiritsuku
1
"Les macros nous donnent toutes les garanties de temps de compilation" - c'est
époustouflant