タイプダイナミックはどのように機能し、どのように使用しますか?


95

DynamicScalaで動的型付けを行うことはどういうわけか可能だと聞きました。しかし、それがどのように見えるのか、それがどのように機能するのかは想像できません。

特性から継承できることがわかりました Dynamic

class DynImpl extends Dynamic

APIは、 1はこのようにそれを使用できることを述べています:

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定義されたメソッドはなく、自分で実装する方法を想像できません。

誰かがそれを機能させるために何をする必要があるかを私に示すことができますか?

回答:


188

Scalas型をDynamic使用すると、存在しないオブジェクトのメソッドを呼び出すことができます。つまり、動的言語の「メソッドが見つからない」のレプリカです。

正解です。scala.Dynamicメンバーはなく、単なるマーカーインターフェイスです。具体的な実装はコンパイラによって埋められます。Scalas 文字列補間機能に関しては、生成された実装を説明する明確に定義されたルールがあります。実際、4つの異なる方法を実装できます。

  • selectDynamic -フィールドアクセサーを記述できます。 foo.bar
  • updateDynamic -フィールドの更新を書き込むことができます: foo.bar = 0
  • applyDynamic -引数を使用してメソッドを呼び出すことができます: foo.bar(0)
  • applyDynamicNamed -名前付き引数でメソッドを呼び出すことができます: foo.bar(f = 0)

これらのメソッドの1つを使用するには、拡張するクラスを記述し、Dynamicそこでメソッドを実装するだけで十分です。

class DynImpl extends Dynamic {
  // method implementations here
}

さらに、追加する必要があります

import scala.language.dynamics

または-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

ご覧のとおり、動的メソッドを明示的に呼び出すことも可能です。

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実行時に。しかし、動的言語でさえこれは危険な機能であるため、これはそれほど悪くありません。

ここでもう1つ注意する点は、とupdateDynamic一緒に実装する必要があることselectDynamicです。これを行わないと、コンパイルエラーが発生します。このルールは、同じ名前のGetterがある場合にのみ機能する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 = 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は、さらにいくつかのリソースがあります。


1
間違いなく素晴らしい答えとScala Powerのショーケース
Herrington Darkholme 2014年

機能がデフォルトで非表示になっている場合、私はそれをパワーとは呼びません。
matanster 2015

Scala Dynamicのパフォーマンスに関する情報はありますか?私はScalaリフレクションが遅いことを知っています(そのため、Scalaマクロになります)Scala Dynamicを使用するとパフォーマンスが劇的に低下しますか?
ウィンドウェラー2015

1
@AllenNie私の回答でわかるように、それを実装するにはさまざまな方法があります。マクロを使用する場合、動的な呼び出しはコンパイル時に解決されるため、オーバーヘッドはありません。実行時にdoチェックを使用する場合は、正しいコードパスに正しくディスパッチするためにパラメーターチェックを行う必要があります。これは、アプリケーションの他のパラメーターチェックよりもオーバーヘッドが大きくなることはありません。リフレクションを利用すると、明らかにオーバーヘッドが増えますが、アプリケーションの速度がどれだけ低下するかを自分で測定する必要があります。
キリツク2015

1
「マクロはすべてのコンパイル時間の保証を私たちに返します」-これは私の心を吹き飛ばしています
tksfz
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.