Тип Scalas Dynamicдозволяє викликати методи на об'єктах, які не існують, або іншими словами, це копія "відсутнього методу" в динамічних мовах.
Це правильно, scala.Dynamicне має жодних членів, це просто інтерфейс маркера - конкретна реалізація заповнюється компілятором. Що стосується функції Scalas String Interpolation, є чітко визначені правила, що описують згенеровану реалізацію. Насправді можна реалізувати чотири різні методи:
selectDynamic - дозволяє писати польові пристосування: foo.bar
updateDynamic - дозволяє писати оновлення полів: foo.bar = 0
applyDynamic - дозволяє викликати методи з аргументами: foo.bar(0)
applyDynamicNamed - дозволяє викликати методи з іменованими аргументами: foo.bar(f = 0)
Для використання одного з цих методів достатньо написати клас, що розширюється, Dynamicі реалізувати там методи:
class DynImpl extends Dynamic {
// method implementations here
}
Крім того, потрібно додати a
import scala.language.dynamics
або встановіть параметр компілятора, -language:dynamicsоскільки ця функція за замовчуванням прихована.
selectDynamic
selectDynamicє найпростішим у реалізації. Компілятор перекладає виклик foo.barto foo.selectDynamic("bar"), тому потрібно, щоб цей метод мав список аргументів, очікуючи 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
Як бачимо, можна також явно викликати динамічні методи.
updateDynamic
Оскільки updateDynamicвикористовується для оновлення значення, цей метод повинен повернути Unit. Крім того, ім'я поля для оновлення та його значення передаються компілятором у різні списки аргументів:
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
Код працює належним чином - можна додати методи під час виконання до коду. З іншого боку, код більше не безпечний для типів, і якщо викликається метод, який не існує, він повинен оброблятися також під час виконання. Крім того, цей код не такий корисний, як у динамічних мовах, оскільки неможливо створити методи, які слід викликати під час виконання. Це означає, що ми не можемо робити щось подібне
val name = "foo"
d.$name
де d.$nameбуде перетворено d.fooна час виконання. Але це не так погано, оскільки навіть у динамічних мовах це небезпечна особливість.
Ще одне, на що слід звернути увагу, - це те, що updateDynamicпотрібно впроваджувати разом із selectDynamic. Якщо ми цього не зробимо, ми отримаємо помилку компіляції - це правило схоже на реалізацію Setter, яке працює лише за наявності Getter з тим самим іменем.
applyDynamic
Можливість викликати методи з аргументами забезпечується 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
Ім'я методу та його аргументи знову розділяються на різні списки параметрів. Ми можемо викликати довільні методи з довільною кількістю аргументів, якщо хочемо, але якщо хочемо викликати метод без будь-яких дужок, які нам потрібно реалізувати selectDynamic.
Підказка: Також можна використовувати синтаксис apply з applyDynamic:
scala> d(5)
res1: String = method 'apply' called with arguments '5'
applyDynamicNamed
Останній доступний метод дозволяє назвати наші аргументи, якщо ми хочемо:
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)'
Різниця в сигнатурі методу полягає в тому, що applyDynamicNamedочікуються кортежі форми, (String, A)де Aє довільний тип.
Всі перераховані вище методи мають спільне, що їх параметри можна параметризувати:
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
На щастя, можна також додати неявні аргументи - якщо ми додаємо TypeTagконтекст, ми можемо легко перевірити типи аргументів. І найкраще, що навіть тип повернення правильний - хоча нам довелося додати кілька приводів.
Але Скала не була б Скалою, коли немає можливості знайти шлях до таких вад. У нашому випадку ми можемо використовувати класи класів, щоб уникнути закидів:
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: _*)
}
}
Хоча реалізація виглядає не так приємно, її потужність не може бути сумнівною:
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
Крім того, можна також поєднувати Dynamicз макросами:
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")
^
Макроси повертають нам усі гарантії часу компіляції, і хоча це не так корисно у наведеному вище випадку, можливо, це може бути дуже корисно для деяких Scala DSL.
Якщо ви хочете отримати ще більше інформації про Dynamicце, є ще кілька ресурсів: