Як працює тип Dynamic і як ним користуватися?


95

Я чув, що за допомогою Dynamicпевної програми можна динамічно набирати текст у Scala. Але я не уявляю, як це може виглядати або як це працює.

Я з’ясував, що можна успадкувати за ознакою Dynamic

class DynImpl extends Dynamic

API каже , що можна використовувати його як це:

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

Але коли я пробую це не працює:

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

Це цілком логічно, адже після перегляду джерел виявилося, що ця риса абсолютно порожня. Не визначено жодного методу, applyDynamicі я не уявляю, як його реалізувати самостійно.

Хтось може показати мені, що мені потрібно зробити, щоб це запрацювало?

Відповіді:


188

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


1
Безумовно, чудова відповідь і вітрина Scala Power
Herrington Darkholme

Я б не називав це енергією, якщо функція прихована за замовчуванням, наприклад, може бути експериментальною або погано грати з іншими, чи це так?
matanster

Чи є інформація про продуктивність Scala Dynamic? Я знаю, що Scala Reflection відбувається повільно (отже, з’являється Scala-macro). Чи буде використання Scala Dynamic різко уповільнити продуктивність?
windweller

1
@AllenNie Як ви можете бачити в моїй відповіді, існують різні способи його реалізації. Якщо ви використовуєте макроси, накладних витрат більше не буде, оскільки динамічний виклик вирішується під час компіляції. Якщо ви використовуєте do check під час виконання, вам доведеться виконати перевірку параметрів, щоб правильно відправити до правильного шляху коду. Це не повинно бути накладнішими, ніж будь-які інші параметри перевірки у вашому додатку. Якщо ви використовуєте роздуми, ви, очевидно, отримуєте більше накладних витрат, але вам доведеться самостійно виміряти, наскільки це уповільнює вашу заявку.
kiritsuku

1
"Макроси повертають нам усі гарантії часу компіляції" - це мене
вражає
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.