Тип 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.bar
to 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
це, є ще кілька ресурсів: