1. ホーム
  2. scala

[解決済み] タイプダイナミックの仕組みと使い方を教えてください。

2022-12-13 15:48:38

質問

と聞いたのですが 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 というメソッドも定義されておらず、自分で実装する方法が思いつきません。

どなたか、動作させるために必要なことを教えていただけませんか?

どのように解決するのですか?

スカラタイプ Dynamic は、存在しないオブジェクトのメソッドを呼び出すことができます。言い換えれば、これは動的言語における "method missing" の複製です。

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

  • 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.barfoo.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 に変換される。しかし、動的言語であってもこれは危険な機能なので、それほど悪いことではありません。

ここでもう一つ注意しなければならないのは 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

メソッド名とその引数は、また別のパラメータリストに分離されます。必要であれば任意の数の引数で任意のメソッドを呼び出すことができますが、もし括弧なしでメソッドを呼び出したいのであれば、実装として 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 についてさらに情報を得たい場合は、いくつかのリソースがあります。