Bagaimana cara kerja tipe Dynamic dan bagaimana cara menggunakannya?

95

Saya mendengar bahwa dengan Dynamicitu entah bagaimana mungkin untuk melakukan pengetikan dinamis dalam Scala. Tapi saya tidak bisa membayangkan bagaimana tampilannya atau bagaimana cara kerjanya.

Saya menemukan bahwa seseorang dapat mewarisi dari sifat Dynamic

class DynImpl extends Dynamic

The API mengatakan bahwa salah satu dapat menggunakannya seperti ini:

foo.method ("blah") ~~> foo.applyDynamic ("method") ("blah")

Tetapi ketika saya mencobanya, itu tidak berhasil:

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")
               ^

Ini sepenuhnya logis, karena setelah melihat ke sumbernya , ternyata sifat ini benar-benar kosong. Tidak ada metode yang applyDynamicditentukan dan saya tidak dapat membayangkan bagaimana menerapkannya sendiri.

Dapatkah seseorang menunjukkan kepada saya apa yang harus saya lakukan agar berhasil?

Kiritsuku
sumber

Jawaban:

188

Jenis Scalas Dynamicmemungkinkan Anda memanggil metode pada objek yang tidak ada atau dengan kata lain merupakan replika "metode yang hilang" dalam bahasa dinamis.

Benar, scala.Dynamictidak memiliki anggota, ini hanya antarmuka penanda - implementasi konkret diisi oleh kompiler. Adapun fitur Interpolasi String Scalas ada aturan yang didefinisikan dengan baik yang menjelaskan implementasi yang dihasilkan. Faktanya, seseorang dapat menerapkan empat metode berbeda:

  • selectDynamic - memungkinkan untuk menulis pengakses bidang: foo.bar
  • updateDynamic - memungkinkan untuk menulis pembaruan lapangan: foo.bar = 0
  • applyDynamic - memungkinkan untuk memanggil metode dengan argumen: foo.bar(0)
  • applyDynamicNamed - memungkinkan untuk memanggil metode dengan argumen bernama: foo.bar(f = 0)

Untuk menggunakan salah satu metode ini, cukup menulis kelas yang diperluas Dynamicdan mengimplementasikan metode di sana:

class DynImpl extends Dynamic {
  // method implementations here
}

Selanjutnya perlu menambahkan file

import scala.language.dynamics

atau setel opsi kompiler -language:dynamicskarena fitur tersebut tersembunyi secara default.

selectDynamic

selectDynamicadalah yang termudah untuk diterapkan. Kompilator menerjemahkan panggilan foo.barke foo.selectDynamic("bar"), oleh karena itu metode ini diharuskan memiliki daftar argumen yang mengharapkan 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

Seperti yang bisa dilihat, metode dinamis juga dapat dipanggil secara eksplisit.

updateDynamic

Karena updateDynamicdigunakan untuk memperbarui nilai yang perlu dikembalikan metode ini Unit. Selanjutnya, nama bidang yang akan diperbarui dan nilainya diteruskan ke daftar argumen yang berbeda oleh kompilator:

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

Kode berfungsi seperti yang diharapkan - dimungkinkan untuk menambahkan metode pada waktu proses ke kode. Di sisi lain, kode tersebut tidak lagi aman untuk diketik dan jika sebuah metode dipanggil tetapi tidak ada, ini harus ditangani pada waktu proses juga. Selain itu, kode ini tidak berguna seperti dalam bahasa dinamis karena tidak mungkin membuat metode yang harus dipanggil saat runtime. Artinya kita tidak bisa melakukan sesuatu seperti itu

val name = "foo"
d.$name

di mana d.$nameakan diubah menjadi d.foosaat runtime. Tetapi ini tidak terlalu buruk karena bahkan dalam bahasa dinamis ini adalah fitur yang berbahaya.

Hal lain yang perlu diperhatikan disini, adalah yang updateDynamicperlu diimplementasikan bersama selectDynamic. Jika kita tidak melakukan ini, kita akan mendapatkan kesalahan kompilasi - aturan ini mirip dengan implementasi Setter, yang hanya berfungsi jika ada Getter dengan nama yang sama.

applyDynamic

Kemampuan untuk memanggil metode dengan argumen disediakan oleh 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

Nama metode dan argumennya lagi-lagi dipisahkan ke daftar parameter yang berbeda. Kita dapat memanggil metode arbitrer dengan sejumlah argumen jika kita mau, tetapi jika kita ingin memanggil metode tanpa tanda kurung, kita perlu menerapkannya selectDynamic.

Petunjuk: Dimungkinkan juga untuk menggunakan apply-syntax dengan applyDynamic:

scala> d(5)
res1: String = method 'apply' called with arguments '5'

applyDynamicNamed

Metode terakhir yang tersedia memungkinkan kita memberi nama argumen kita jika kita ingin:

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)'

Perbedaan dalam tanda tangan metode adalah yang applyDynamicNamedmengharapkan tupel dari bentuk di (String, A)mana Amerupakan tipe arbitrer.


Semua metode di atas memiliki kesamaan bahwa parameternya dapat dijadikan parameter:

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

Untungnya, dimungkinkan juga untuk menambahkan argumen implisit - jika kita menambahkan TypeTagikatan konteks, kita dapat dengan mudah memeriksa tipe argumen. Dan yang terbaik adalah bahwa bahkan tipe pengembaliannya benar - meskipun kami harus menambahkan beberapa gips.

Tetapi Scala tidak akan menjadi Scala jika tidak ada cara untuk menemukan cara mengatasi kekurangan tersebut. Dalam kasus kami, kami dapat menggunakan kelas tipe untuk menghindari gips:

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: _*)
  }

}

Meskipun implementasinya tidak terlihat bagus, kekuatannya tidak dapat dipertanyakan:

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

Di atas semua, dimungkinkan juga untuk menggabungkan Dynamicdengan makro:

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")
                       ^

Makro memberi kita kembali semua jaminan waktu kompilasi dan meskipun tidak begitu berguna dalam kasus di atas, mungkin ini bisa sangat berguna untuk beberapa Scala DSL.

Jika Anda ingin mendapatkan lebih banyak informasi tentang Dynamicada lebih banyak sumber:

Kiritsuku
sumber
1
Jelas jawaban yang bagus dan karya Scala Power
Herrington Darkholme
Saya tidak akan menyebutnya daya jika fitur tersebut disembunyikan secara default, misalnya mungkin eksperimental atau tidak berfungsi dengan baik dengan yang lain, atau apakah itu?
matanster
Apakah ada informasi tentang kinerja Scala Dynamic? Saya tahu Scala Reflection lambat (karena itu muncul Scala-macro). Akankah penggunaan Scala Dynamic memperlambat kinerja secara dramatis?
windweller
1
@AllenNie Seperti yang Anda lihat di jawaban saya, ada berbagai cara untuk mengimplementasikannya. Jika Anda menggunakan makro, tidak ada overhead lagi, karena panggilan dinamis diselesaikan pada waktu kompilasi. Jika Anda menggunakan melakukan pemeriksaan pada waktu proses, Anda harus melakukan pemeriksaan parameter untuk mengirimkan dengan benar ke jalur kode yang benar. Itu seharusnya tidak menjadi overhead lebih dari pemeriksaan parameter lain yang dimiliki aplikasi Anda. Jika Anda menggunakan refleksi, Anda jelas mendapatkan lebih banyak overhead tetapi Anda harus mengukur sendiri seberapa banyak itu memperlambat aplikasi Anda.
kiritsuku
1
"Makro mengembalikan semua jaminan waktu kompilasi" - ini
mengejutkan