【问题标题】:How does type Dynamic work and how to use it?动态类型如何工作以及如何使用它?
【发布时间】:2013-03-25 20:30:44
【问题描述】:

我听说使用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")
               ^

这完全合乎逻辑,因为在查看了sources 之后,发现这个 trait 完全是空的。没有定义方法applyDynamic,我无法想象如何自己实现。

谁能告诉我我需要做什么才能让它工作?

【问题讨论】:

    标签: scala


    【解决方案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 是最容易实现的。编译器将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 用于更新值,所以此方法需要返回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 提供了使用参数调用方法的能力:

    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-syntax 与 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 的更多信息,还有更多资源:

    【讨论】:

    • 绝对是一个很好的答案和 Scala Power 的展示
    • 我不会称它为 power,以防该功能默认隐藏,例如可能是实验性的,或者与其他人相处得不好,或者是吗?
    • 有没有关于Scala Dynamic 性能的信息?我知道 Scala 反射很慢(因此出现了 Scala 宏)。使用 Scala Dynamic 会显着降低性能吗?
    • @AllenNie 正如您在我的回答中看到的,有不同的方法来实现它。如果您使用宏,则不再有任何开销,因为动态调用是在编译时解决的。如果在运行时使用 do 检查,则必须进行参数检查才能正确分派到正确的代码路径。这不应该比应用程序中任何其他参数检查的开销更大。如果您使用反射,您显然会获得更多开销,但您必须自己衡量它会减慢您的应用程序的速度。
    • “宏给我们所有的编译时间保证”——这让我大吃一惊
    【解决方案2】:

    kiritsuku 的回答更好。这是为了展示一个实用的 TL;DR 用例。

    Dynamics 可用于使用构建器模式动态构造对象。

    import scala.language.dynamics
    
    case class DynImpl(
      inputParams: Map[String, List[String]] = Map.empty[String, List[String]]
    ) extends Dynamic {
    
      def applyDynamic(name: String)(args: String*): DynImpl = {
        copy(inputParams = inputParams ++ Map(name -> args.toList))
      }
    
    }
    
    val d1 = DynImpl().whatever("aaa", "bbb").cool("ccc")
    println(d1.inputParams) // Map(whatever -> List(aaa, bbb), cool -> List(ccc))
    
    val d2 = DynImpl().whatever("aaa", "bbb").fun("haha")
    println(d2.inputParams) // Map(whatever -> List(aaa, bbb), fun -> List(haha))
    

    【讨论】:

      猜你喜欢
      • 2015-04-04
      • 2015-05-28
      • 2014-11-15
      • 2011-07-30
      • 2011-05-04
      • 2018-05-14
      • 2011-11-17
      • 2011-10-27
      相关资源
      最近更新 更多