类型动态如何工作以及如何使用它?

kir*_*uku 91 scala

我听说Dynamic在某种程度上可以在Scala中进行动态类型化.但我无法想象这可能是什么样子或它是如何工作的.

我发现一个人可以继承特质 Dynamic

class DynImpl extends Dynamic
Run Code Online (Sandbox Code Playgroud)

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")
               ^
Run Code Online (Sandbox Code Playgroud)

这是完全合乎逻辑的,因为在查看来源之后,事实证明这个特征是完全空的.没有applyDynamic定义方法,我无法想象如何自己实现它.

有人能告诉我我需要做些什么才能让它发挥作用吗?

kir*_*uku 181

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
}
Run Code Online (Sandbox Code Playgroud)

此外,需要添加一个

import scala.language.dynamics
Run Code Online (Sandbox Code Playgroud)

或者设置编译器选项,-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
Run Code Online (Sandbox Code Playgroud)

可以看出,也可以明确地调用动态方法.

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
Run Code Online (Sandbox Code Playgroud)

代码按预期工作 - 可以在运行时向代码添加方法.另一方面,代码不再是类型安全的,如果调用的方法不存在,则必须在运行时处理.此外,此代码不像动态语言那样有用,因为无法创建应在运行时调用的方法.这意味着我们不能做类似的事情

val name = "foo"
d.$name
Run Code Online (Sandbox Code Playgroud)

哪里d.$named.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
Run Code Online (Sandbox Code Playgroud)

方法的名称及其参数再次分为不同的参数列表.如果需要,我们可以使用任意数量的参数调用任意方法,但是如果我们想要调用没有任何括号的方法,我们需要实现selectDynamic.

提示:也可以使用apply-syntax applyDynamic:

scala> d(5)
res1: String = method 'apply' called with arguments '5'
Run Code Online (Sandbox Code Playgroud)

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)'
Run Code Online (Sandbox Code Playgroud)

在该方法的签名不同的是,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
Run Code Online (Sandbox Code Playgroud)

幸运的是,也可以添加隐式参数 - 如果我们添加一个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: _*)
  }

}
Run Code Online (Sandbox Code Playgroud)

虽然实现看起来不那么好,但其功能不容质疑:

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
Run Code Online (Sandbox Code Playgroud)

最重要的是,它也可以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")
                       ^
Run Code Online (Sandbox Code Playgroud)

宏给了我们所有的编译时保证,虽然它在上面的例子中没用,但它可能对某些Scala DSL非常有用.

如果您想获得有关Dynamic更多资源的更多信息:

  • 很好的答案,谢谢你的写作! (8认同)