Как работает тип Dynamic и как его использовать?

95

Я слышал, что с Dynamicего помощью как-то можно делать динамическую типизацию в Scala. Но я не могу представить, как это может выглядеть или как это работает.

Я выяснил, что от черты можно унаследовать Dynamic

class DynImpl extends Dynamic

API говорит , что можно использовать его как это:

foo.method ("бла") ~~> foo.applyDynamic ("метод") ("бла")

Но когда я пробую, это не работает:

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
}

Кроме того, нужно добавить

import scala.language.dynamics

или установите параметр компилятора, -language:dynamicsпотому что функция по умолчанию скрыта.

selectDynamic

selectDynamicсамый простой в реализации. Компилятор переводит вызов foo.barв 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.

Подсказка: также можно использовать синтаксис применения с 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контекстную привязку, мы сможем легко проверить типы аргументов. И самое лучшее, что даже тип возвращаемого значения правильный - хотя нам пришлось добавить несколько приведений.

Но Scala не будет Scala, если нет способа найти способ обойти такие недостатки. В нашем случае мы можем использовать классы типов, чтобы избежать приведения типов:

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
Херрингтон Даркхолм
Я бы не назвал это мощностью, если функция по умолчанию скрыта, например, может быть экспериментальной или не очень хорошо работает с другими, или нет?
Matanster
Есть ли информация о производительности Scala Dynamic? Я знаю, что Scala Reflection медленный (отсюда и Scala-macro). Не приведет ли использование Scala Dynamic к значительному снижению производительности?
windweller
1
@AllenNie Как вы можете видеть из моего ответа, есть разные способы реализовать это. Если вы используете макросы, больше нет накладных расходов, поскольку динамический вызов разрешается во время компиляции. Если вы используете проверки do во время выполнения, вы должны выполнить проверку параметров, чтобы правильно отправить по правильному пути кода. Это не должно быть больше накладных расходов, чем любая другая проверка параметров в вашем приложении. Если вы используете отражение, очевидно, что у вас будет больше накладных расходов, но вам придется самостоятельно измерить, насколько это замедляет работу вашего приложения.
kiritsuku
1
«Макросы возвращают нам все гарантии времени компиляции» - это просто
шокирует