Ответ 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
, есть еще несколько ресурсов:
- Официальное предложение SIP введшее
Dynamic
в Scala - Практическое использование динамического типа в Scala - еще один вопрос о SO (но очень устаревший)