Wie funktioniert Type Dynamic und wie wird es verwendet?

94

Ich habe gehört, dass Dynamices damit irgendwie möglich ist, in Scala dynamisch zu tippen. Aber ich kann mir nicht vorstellen, wie das aussehen könnte oder wie es funktioniert.

Ich fand heraus, dass man von Eigenschaften erben kann Dynamic

class DynImpl extends Dynamic

Die API sagt, dass man es so verwenden kann:

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

Aber wenn ich es ausprobiere, funktioniert es nicht:

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

Dies ist völlig logisch, da sich nach Betrachtung der Quellen herausstellte, dass dieses Merkmal vollständig leer ist. Es ist keine Methode applyDynamicdefiniert und ich kann mir nicht vorstellen, wie ich sie selbst implementieren soll.

Kann mir jemand zeigen, was ich tun muss, damit es funktioniert?

kiritsuku
quelle

Antworten:

187

Mit dem Skalentyp Dynamickönnen Sie Methoden für Objekte aufrufen, die nicht vorhanden sind. Mit anderen Worten, es handelt sich um eine Replik von "Methode fehlt" in dynamischen Sprachen.

Es ist richtig, scala.Dynamichat keine Mitglieder, es ist nur eine Markierungsschnittstelle - die konkrete Implementierung wird vom Compiler ausgefüllt. Wie für Scalas String Interpolation- Funktion gibt es genau definierte Regeln, die die generierte Implementierung beschreiben. Tatsächlich kann man vier verschiedene Methoden implementieren:

  • selectDynamic - ermöglicht das Schreiben von Feldzugängern: foo.bar
  • updateDynamic - ermöglicht das Schreiben von Feldaktualisierungen: foo.bar = 0
  • applyDynamic - Ermöglicht das Aufrufen von Methoden mit Argumenten: foo.bar(0)
  • applyDynamicNamed - Ermöglicht das Aufrufen von Methoden mit benannten Argumenten: foo.bar(f = 0)

Um eine dieser Methoden zu verwenden, reicht es aus, eine erweiterte Klasse zu schreiben Dynamicund die Methoden dort zu implementieren:

class DynImpl extends Dynamic {
  // method implementations here
}

Außerdem muss man a hinzufügen

import scala.language.dynamics

oder setzen Sie die Compiler-Option -language:dynamics da die Funktion standardmäßig ausgeblendet ist.

selectDynamic

selectDynamicist am einfachsten zu implementieren. Der Compiler übersetzt einen Aufruf von foo.barto foo.selectDynamic("bar"), daher ist es erforderlich, dass diese Methode eine Argumentliste hat, die Folgendes erwartet 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

Wie man sieht, ist es auch möglich, die dynamischen Methoden explizit aufzurufen.

updateDynamic

Da updateDynamicdiese Methode zum Aktualisieren eines Werts verwendet wird, muss sie zurückgegeben werden Unit. Darüber hinaus werden der Name des zu aktualisierenden Felds und sein Wert vom Compiler an verschiedene Argumentlisten übergeben:

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

Der Code funktioniert wie erwartet - es ist möglich, dem Code zur Laufzeit Methoden hinzuzufügen. Auf der anderen Seite ist der Code nicht mehr typsicher, und wenn eine Methode aufgerufen wird, die nicht existiert, muss dies auch zur Laufzeit behandelt werden. Außerdem ist dieser Code nicht so nützlich wie in dynamischen Sprachen, da es nicht möglich ist, die Methoden zu erstellen, die zur Laufzeit aufgerufen werden sollen. Das heißt, wir können so etwas nicht machen

val name = "foo"
d.$name

wo d.$namewürde zur d.fooLaufzeit umgewandelt werden. Das ist aber nicht so schlimm, denn selbst in dynamischen Sprachen ist dies eine gefährliche Funktion.

Eine andere Sache, die hier zu beachten ist, ist, dass updateDynamiczusammen mit implementiert werden mussselectDynamic . Wenn wir dies nicht tun, erhalten wir einen Kompilierungsfehler - diese Regel ähnelt der Implementierung eines Setters, die nur funktioniert, wenn es einen Getter mit demselben Namen gibt.

applyDynamic

Die Möglichkeit, Methoden mit Argumenten aufzurufen, wird bereitgestellt durch 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

Der Name der Methode und ihre Argumente werden wiederum in verschiedene Parameterlisten unterteilt. Wir können beliebige Methoden mit einer beliebigen Anzahl von Argumenten aufrufen, wenn wir möchten, aber wenn wir eine Methode ohne Klammern aufrufen möchten, müssen wir sie implementierenselectDynamic .

Hinweis: Es ist auch möglich, die Apply-Syntax zu verwenden mit applyDynamic:

scala> d(5)
res1: String = method 'apply' called with arguments '5'

applyDynamicNamed

Mit der letzten verfügbaren Methode können wir unsere Argumente benennen, wenn wir möchten:

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

Der Unterschied in der Methodensignatur besteht darin, applyDynamicNameddass Tupel der Form erwartet werden, bei (String, A)denen Aes sich um einen beliebigen Typ handelt.


Allen oben genannten Methoden ist gemeinsam, dass ihre Parameter parametriert werden können:

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

Glücklicherweise ist es auch möglich, implizite Argumente hinzuzufügen. Wenn wir eine TypeTagKontextbindung hinzufügen , können wir die Arten der Argumente leicht überprüfen. Und das Beste ist, dass sogar der Rückgabetyp korrekt ist - obwohl wir einige Casts hinzufügen mussten.

Aber Scala wäre keine Scala, wenn es keinen Weg gibt, solche Fehler zu umgehen. In unserem Fall können wir Typklassen verwenden, um die Casts zu vermeiden:

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: _*)
  }

}

Obwohl die Implementierung nicht so gut aussieht, kann ihre Leistungsfähigkeit nicht in Frage gestellt werden:

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

Darüber hinaus ist es auch möglich, Dynamicmit Makros zu kombinieren :

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

Makros geben uns alle Garantien für die Kompilierungszeit zurück, und obwohl dies im obigen Fall nicht so nützlich ist, kann es für einige Scala-DSLs möglicherweise sehr nützlich sein.

Wenn Sie noch mehr Informationen darüber erhalten möchten, Dynamicgibt es einige weitere Ressourcen:

kiritsuku
quelle
1
Auf jeden Fall eine großartige Antwort und ein Schaufenster von Scala Power
Herrington Darkholme
Ich würde es nicht Power nennen, wenn die Funktion standardmäßig ausgeblendet ist, z. B. experimentell ist oder nicht gut mit anderen spielt, oder?
Matanster
Gibt es Informationen zur Leistung von Scala Dynamic? Ich weiß, dass die Scala-Reflexion langsam ist (daher kommt das Scala-Makro). Wird die Verwendung von Scala Dynamic die Leistung drastisch verlangsamen?
Windweller
1
@AllenNie Wie Sie in meiner Antwort sehen können, gibt es verschiedene Möglichkeiten, dies zu implementieren. Wenn Sie Makros verwenden, entsteht kein Overhead mehr, da der dynamische Aufruf zur Kompilierungszeit aufgelöst wird. Wenn Sie zur Laufzeit do-Prüfungen verwenden, müssen Sie die Parameterprüfung durchführen, um korrekt an den richtigen Codepfad zu senden. Das sollte nicht mehr Aufwand bedeuten als jede andere Parameterprüfung in Ihrer Anwendung. Wenn Sie Reflexion verwenden, erhalten Sie offensichtlich mehr Overhead, müssen jedoch selbst messen, um wie viel Ihre Anwendung dadurch verlangsamt wird.
Kiritsuku
1
"Makros geben uns alle Garantien für die Kompilierungszeit zurück" - das ist
unglaublich