Jak działa Type Dynamic i jak go używać?


97

Słyszałem, że Dynamicw Scali da się jakoś dynamicznie pisać. Ale nie mogę sobie wyobrazić, jak to może wyglądać ani jak to działa.

Dowiedziałem się, że można odziedziczyć cechę Dynamic

class DynImpl extends Dynamic

API mówi, że można go używać tak:

foo.method ("bla") ~~> foo.applyDynamic ("metoda") ("bla")

Ale kiedy go wypróbuję, nie działa:

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

Jest to całkowicie logiczne, bo po przejrzeniu źródeł okazało się, że ta cecha jest zupełnie pusta. Nie ma applyDynamicokreślonej metody i nie wyobrażam sobie, jak sama ją wdrożyć.

Czy ktoś może mi pokazać, co muszę zrobić, aby to zadziałało?

Odpowiedzi:


190

Typ Scalas Dynamicumożliwia wywoływanie metod na obiektach, które nie istnieją lub innymi słowy jest to replika „brakującej metody” w językach dynamicznych.

Jest poprawny, scala.Dynamicnie ma żadnych składowych, to tylko interfejs znacznika - konkretna implementacja jest wypełniana przez kompilator. Jeśli chodzi o funkcję Scalas String Interpolation, istnieją dobrze zdefiniowane reguły opisujące wygenerowaną implementację. W rzeczywistości można zaimplementować cztery różne metody:

  • selectDynamic - umożliwia pisanie akcesorów do pól: foo.bar
  • updateDynamic - umożliwia zapisywanie aktualizacji pola: foo.bar = 0
  • applyDynamic - pozwala na wywołanie metod z argumentami: foo.bar(0)
  • applyDynamicNamed - pozwala na wywołanie metod z nazwanymi argumentami: foo.bar(f = 0)

Aby skorzystać z jednej z tych metod, wystarczy napisać klasę rozszerzającą Dynamici zaimplementować tam metody:

class DynImpl extends Dynamic {
  // method implementations here
}

Ponadto należy dodać

import scala.language.dynamics

lub ustaw opcję kompilatora -language:dynamics ponieważ funkcja jest domyślnie ukryta.

wybierz Dynamiczny

selectDynamicjest najłatwiejszy do wdrożenia. Kompilator tłumaczy wywołanie funkcji foo.barna foo.selectDynamic("bar"), dlatego wymagane jest, aby ta metoda miała listę argumentów oczekującą 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

Jak widać, możliwe jest również jawne wywołanie metod dynamicznych.

updateDynamic

Ponieważ updateDynamicjest używany do aktualizowania wartości, ta metoda musi zwrócić Unit. Ponadto nazwa pola do aktualizacji i jego wartość są przekazywane przez kompilator do różnych list argumentów:

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

Kod działa zgodnie z oczekiwaniami - możliwe jest dodawanie metod w czasie wykonywania do kodu. Z drugiej strony kod nie jest już bezpieczny dla typów i jeśli wywoływana jest metoda, która nie istnieje, musi to być również obsługiwane w czasie wykonywania. Ponadto ten kod nie jest tak przydatny jak w językach dynamicznych, ponieważ nie jest możliwe utworzenie metod, które powinny być wywoływane w czasie wykonywania. Oznacza to, że nie możemy zrobić czegoś takiego

val name = "foo"
d.$name

gdzie d.$namezostanie przekształconyd.foo w czasie wykonywania. Ale to nie jest takie złe, ponieważ nawet w językach dynamicznych jest to niebezpieczna funkcja.

Kolejną rzeczą, na którą należy zwrócić uwagę, jest to, że updateDynamicnależy to wdrożyć razem zselectDynamic . Jeśli tego nie zrobimy, otrzymamy błąd kompilacji - ta reguła jest podobna do implementacji Settera, który działa tylko wtedy, gdy istnieje Getter o tej samej nazwie.

ApplyDynamic

Możliwość wywoływania metod z argumentami zapewnia 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

Nazwa metody i jej argumenty są ponownie rozdzielane na różne listy parametrów. Możemy wywołać dowolne metody z dowolną liczbą argumentów, jeśli chcemy, ale jeśli chcemy wywołać metodę bez nawiasów, musimy zaimplementować selectDynamic.

Wskazówka: możliwe jest również użycie składni zastosuj z applyDynamic:

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

applyDynamicNamed

Ostatnia dostępna metoda pozwala nam nazwać nasze argumenty, jeśli chcemy:

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

Różnica w sygnaturze metody polega na tym, że applyDynamicNamedoczekuje się krotek formularza, w (String, A)którym Ajest to dowolny typ.


Cechą wspólną wszystkich powyższych metod jest możliwość parametryzacji ich parametrów:

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

Na szczęście możliwe jest również dodanie argumentów niejawnych - jeśli dodamy TypeTagpowiązanie kontekstu, możemy łatwo sprawdzić typy argumentów. A najlepsze jest to, że nawet typ zwracany jest poprawny - mimo że musieliśmy dodać kilka rzutów.

Ale Scala nie byłaby Scalą, gdy nie ma sposobu, aby znaleźć sposób na obejście takich wad. W naszym przypadku możemy użyć klas typów, aby uniknąć rzutowania:

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

}

Chociaż implementacja nie wygląda tak ładnie, nie można kwestionować jej mocy:

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

Przede wszystkim można również łączyć Dynamicz makrami:

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

Makra dają nam wszystkie gwarancje czasu kompilacji i chociaż nie jest to przydatne w powyższym przypadku, może być bardzo przydatne dla niektórych DSL Scala.

Jeśli chcesz uzyskać jeszcze więcej informacji, Dynamicdostępnych jest więcej zasobów:


1
Zdecydowanie świetna odpowiedź i wizytówka Scala Power
Herrington Darkholme

Nie nazwałbym tego mocą w przypadku, gdy funkcja jest domyślnie ukryta, np. Może być eksperymentalna lub nie współpracuje dobrze z innymi, czy tak jest?
matanster

Czy są jakieś informacje na temat wydajności Scala Dynamic? Wiem, że Scala Reflection jest powolna (stąd pochodzi Scala-makro). Czy użycie Scala Dynamic dramatycznie spowolni wydajność?
windweller

1
@AllenNie Jak widać w mojej odpowiedzi, są różne sposoby jej wdrożenia. Jeśli używasz makr, nie ma już narzutów, ponieważ wywołanie dynamiczne jest rozwiązywane w czasie kompilacji. Jeśli używasz do check w czasie wykonywania, musisz sprawdzić parametry, aby poprawnie wysłać do właściwej ścieżki kodu. Nie powinno to być większe niż jakiekolwiek inne sprawdzanie parametrów w Twojej aplikacji. Jeśli korzystasz z odbicia, masz oczywiście większe obciążenie, ale musisz sam zmierzyć, jak bardzo spowalnia to twoją aplikację.
kiritsuku

1
„Makra zwracają nam wszystkie gwarancje czasu kompilacji” - to mnie
zaskakuje
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.