Typ Scalas Dynamic
umoż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.Dynamic
nie 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ą Dynamic
i zaimplementować tam metody:
class DynImpl extends Dynamic {
}
Ponadto należy dodać
import scala.language.dynamics
lub ustaw opcję kompilatora -language:dynamics
ponieważ funkcja jest domyślnie ukryta.
wybierz Dynamiczny
selectDynamic
jest najłatwiejszy do wdrożenia. Kompilator tłumaczy wywołanie funkcji foo.bar
na 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ż updateDynamic
jest 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.$name
zostanie 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 updateDynamic
należ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 applyDynamicNamed
oczekuje się krotek formularza, w (String, A)
którym A
jest 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 TypeTag
powią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ć Dynamic
z 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, Dynamic
dostępnych jest więcej zasobów: