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

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

Я узнал, что наследуется от trait 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, и я не могу представить, как его реализовать сам.

Может кто-нибудь показать мне, что мне нужно сделать, чтобы он работал?

Ответ 1

Тип 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 = [email protected]

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 = [email protected]

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, которое работает только в том случае, если есть Геттер с тем же именем.

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 = [email protected]

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 = [email protected]

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 = [email protected]

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 = [email protected]

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 = [email protected]

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

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

Если вы хотите получить еще больше информации о Dynamic, есть еще несколько ресурсов: