Apa sajakah kasus penggunaan yang menarik untuk jenis metode dependen?

127

Jenis-jenis metode dependen, yang dulunya merupakan fitur eksperimental sebelumnya, kini telah diaktifkan secara default di trunk , dan tampaknya ini tampaknya telah menciptakan kegembiraan di komunitas Scala.

Pada pandangan pertama, tidak segera jelas apa manfaatnya. Heiko Seeberger memposting contoh sederhana tipe metode dependen di sini , yang seperti dapat dilihat di komentar di sana dapat dengan mudah direproduksi dengan parameter tipe pada metode. Jadi itu bukan contoh yang sangat menarik. (Saya mungkin kehilangan sesuatu yang jelas. Tolong perbaiki saya jika demikian.)

Apa saja contoh praktis dan berguna dari kasus penggunaan untuk tipe metode dependen di mana mereka jelas menguntungkan daripada alternatifnya?

Hal menarik apa yang bisa kita lakukan dengan mereka yang tidak mungkin / mudah sebelumnya?

Apa yang mereka beli dari kami atas fitur sistem tipe yang ada?

Juga, apakah tipe metode dependen dianalogikan atau menggambar inspirasi dari fitur apa pun yang ditemukan dalam sistem tipe bahasa mengetik canggih lainnya seperti Haskell, OCaml?

missingfaktor
sumber
Anda mungkin tertarik untuk membaca haskell.org/haskellwiki/Dependent_type
Dan Burton
Terima kasih untuk tautannya, Dan! Saya menyadari tipe dependen secara umum, tetapi konsep tipe metode dependen relatif baru bagi saya.
missingfaktor
Tampak bagi saya bahwa "tipe metode dependen" hanyalah tipe yang bergantung pada satu atau lebih tipe input metode (termasuk jenis objek yang digunakan metode ini); tidak ada yang gila di luar gagasan umum tipe dependen. Mungkin saya melewatkan sesuatu?
Dan Burton
Tidak, Anda tidak, tetapi ternyata saya melakukannya. :-) Saya tidak melihat hubungan antara keduanya sebelumnya. Ini sangat jelas sekarang.
missingfaktor

Jawaban:

112

Kurang lebih setiap penggunaan tipe anggota (mis. Bersarang) dapat menimbulkan kebutuhan akan tipe metode dependen. Secara khusus, saya berpendapat bahwa tanpa jenis metode dependen pola kue klasik lebih dekat menjadi anti-pola.

Jadi apa masalahnya? Jenis bersarang di Scala tergantung pada contoh terlampir mereka. Akibatnya, dengan tidak adanya tipe metode dependen, upaya untuk menggunakannya di luar contoh itu bisa sangat sulit. Ini dapat mengubah desain yang awalnya tampak elegan dan menarik menjadi monster yang mengerikan dan sulit untuk diperbaiki.

Saya akan menggambarkan bahwa dengan latihan yang saya berikan selama kursus pelatihan Advanced Scala saya ,

trait ResourceManager {
  type Resource <: BasicResource
  trait BasicResource {
    def hash : String
    def duplicates(r : Resource) : Boolean
  }
  def create : Resource

  // Test methods: exercise is to move them outside ResourceManager
  def testHash(r : Resource) = assert(r.hash == "9e47088d")  
  def testDuplicates(r : Resource) = assert(r.duplicates(r))
}

trait FileManager extends ResourceManager {
  type Resource <: File
  trait File extends BasicResource {
    def local : Boolean
  }
  override def create : Resource
}

class NetworkFileManager extends FileManager {
  type Resource = RemoteFile
  class RemoteFile extends File {
    def local = false
    def hash = "9e47088d"
    def duplicates(r : Resource) = (local == r.local) && (hash == r.hash)
  }
  override def create : Resource = new RemoteFile
}

Ini adalah contoh dari pola kue klasik: kami memiliki keluarga abstraksi yang secara bertahap disempurnakan melalui hierarki ( ResourceManager/ Resourcedisempurnakan oleh FileManager/ Fileyang pada gilirannya disempurnakan oleh NetworkFileManager/ RemoteFile). Ini contoh mainan, tetapi polanya nyata: digunakan di seluruh kompiler Scala dan digunakan secara luas di plugin Scala Eclipse.

Berikut adalah contoh abstraksi yang digunakan,

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)

Perhatikan bahwa dependensi jalur berarti bahwa kompilator akan menjamin bahwa testHashdan testDuplicatesmetode NetworkFileManagerhanya dapat dipanggil dengan argumen yang sesuai dengannya, yaitu. itu sendiri RemoteFiles, dan tidak ada yang lain.

Tidak dapat disangkal bahwa itu adalah properti yang diinginkan, tetapi misalkan kita ingin memindahkan kode uji ini ke file sumber yang berbeda? Dengan tipe metode dependen, mudah untuk mendefinisikan kembali metode-metode tersebut di luar ResourceManagerhierarki,

def testHash4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.hash == "9e47088d")

def testDuplicates4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.duplicates(r))

Perhatikan penggunaan tipe metode dependen di sini: jenis argumen kedua ( rm.Resource) tergantung pada nilai argumen pertama ( rm).

Dimungkinkan untuk melakukan ini tanpa tipe metode dependen, tetapi ini sangat canggung dan mekanismenya sangat tidak intuitif: Saya telah mengajar kursus ini selama hampir dua tahun sekarang, dan pada waktu itu, tidak ada seorang pun yang datang dengan solusi kerja tanpa alasan.

Cobalah sendiri ...

// Reimplement the testHash and testDuplicates methods outside
// the ResourceManager hierarchy without using dependent method types
def testHash        // TODO ... 
def testDuplicates  // TODO ...

testHash(rf)
testDuplicates(rf)

Setelah beberapa saat berjuang dengan itu Anda mungkin akan menemukan mengapa saya (atau mungkin itu adalah David MacIver, kita tidak dapat mengingat siapa di antara kita yang menciptakan istilah) menyebut ini Bakery of Doom.

Sunting: konsensus adalah bahwa Bakery of Doom adalah koin David MacIver ...

Sebagai bonus: bentuk Scala dari tipe dependen secara umum (dan tipe metode dependen sebagai bagian dari itu) terinspirasi oleh bahasa pemrograman Beta ... mereka muncul secara alami dari semantik bersarang Beta yang konsisten. Saya tidak tahu bahasa pemrograman arus utama apa pun yang memiliki tipe dependen dalam formulir ini. Bahasa seperti Coq, Cayenne, Epigram dan Agda memiliki bentuk pengetikan dependen berbeda yang dalam beberapa hal lebih umum, tetapi berbeda secara signifikan dengan menjadi bagian dari sistem tipe yang, tidak seperti Scala, tidak memiliki subtyping.

Miles Sabin
sumber
2
David MacIver yang menciptakan istilah ini, tetapi bagaimanapun, ini cukup deskriptif. Ini adalah penjelasan fantastis tentang mengapa tipe metode dependen sangat menarik. Kerja bagus!
Daniel Spiewak
Awalnya muncul dalam percakapan antara kami berdua di #scala beberapa waktu yang lalu ... seperti saya katakan saya tidak ingat siapa di antara kita yang mengatakannya terlebih dahulu.
Miles Sabin
Sepertinya ingatanku mempermainkanku ... konsensus apakah itu adalah koin David MacIver.
Miles Sabin
Ya, saya tidak ada di sana (di #scala) pada saat itu, tetapi Jorge ada dan di situlah saya mendapatkan informasi saya.
Daniel Spiewak
Memanfaatkan penyempurnaan anggota tipe abstrak, saya bisa mengimplementasikan fungsi testHash4 tanpa rasa sakit. def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")Saya kira ini dapat dianggap sebagai bentuk lain dari tipe dependen.
Marco van Hilst
53
trait Graph {
  type Node
  type Edge
  def end1(e: Edge): Node
  def end2(e: Edge): Node
  def nodes: Set[Node]
  def edges: Set[Edge]
}

Di tempat lain, kami dapat secara statis menjamin bahwa kami tidak mencampuradukkan node dari dua grafik yang berbeda, misalnya:

def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ... 

Tentu saja, ini sudah berfungsi jika didefinisikan di dalam Graph, tetapi katakanlah kita tidak dapat memodifikasi Graphdan sedang menulis ekstensi "pimp my library" untuk itu.

Tentang pertanyaan kedua: tipe yang diaktifkan oleh fitur ini jauh lebih lemah daripada tipe dependen lengkap (Lihat Pemrograman Ketik Ketergantungan di Agda untuk mengetahui lebih lanjut tentang itu.) Saya rasa saya tidak pernah melihat analogi sebelumnya.

Alexey Romanov
sumber
6

Fitur baru ini diperlukan ketika anggota tipe abstrak konkret digunakan sebagai ganti parameter tipe . Ketika parameter tipe digunakan, dependensi tipe polimorfisme keluarga dapat diekspresikan dalam Scala versi terbaru dan beberapa yang lebih lama, seperti dalam contoh sederhana berikut.

trait C[A]
def f[M](a: C[M], b: M) = b
class C1 extends C[Int]
class C2 extends C[String]

f(new C1, 0)
res0: Int = 0
f(new C2, "")
res1: java.lang.String = 
f(new C1, "")
error: type mismatch;
 found   : C1
 required: C[Any]
       f(new C1, "")
         ^
Shelby Moore III
sumber
Ini tidak berhubungan. Dengan anggota tipe, Anda dapat menggunakan penyempurnaan untuk hasil yang sama: trait C {type A}; def f[M](a: C { type A = M}, b: M) = 0;class CI extends C{type A=Int};class CS extends C{type A=String}dll.
nafg
Bagaimanapun, ini tidak ada hubungannya dengan tipe metode dependen. Ambil contoh Alexey ( stackoverflow.com/a/7860821/333643 ). Menggunakan pendekatan Anda (termasuk versi penyempurnaan yang saya komentari) tidak mencapai sasaran. Ini akan memastikan bahwa n1.Node =: = n2.Node, tetapi tidak akan memastikan keduanya berada dalam grafik yang sama. IIUC DMT memastikan hal ini.
nafg
@ nafg Terima kasih telah menunjukkannya. Saya telah menambahkan kata konkret untuk memperjelas bahwa saya tidak merujuk pada kasus penyempurnaan untuk anggota tipe. Sejauh yang saya bisa lihat, ini masih merupakan kasus penggunaan yang valid untuk tipe metode dependen terlepas dari titik Anda (yang saya tahu) bahwa mereka dapat memiliki kekuatan lebih dalam kasus penggunaan lainnya. Atau apakah saya melewatkan esensi mendasar dari komentar kedua Anda?
Shelby Moore III
3

Saya sedang mengembangkan model untuk interoption dari bentuk pemrograman deklaratif dengan keadaan lingkungan. Detailnya tidak relevan di sini (mis. Detail tentang callback dan kesamaan konseptual dengan model Actor yang dikombinasikan dengan Serializer).

Masalah yang relevan adalah nilai negara disimpan dalam peta hash dan direferensikan oleh nilai kunci hash. Fungsi memasukkan argumen tidak berubah yang merupakan nilai dari lingkungan, dapat memanggil fungsi lain seperti itu, dan menulis status ke lingkungan. Tetapi fungsi tidak diperbolehkan untuk membaca nilai dari lingkungan (jadi kode internal fungsi tidak tergantung pada urutan perubahan negara dan dengan demikian tetap deklaratif dalam pengertian itu). Bagaimana cara mengetik ini di Scala?

Kelas lingkungan harus memiliki metode kelebihan beban yang menginput fungsi seperti itu untuk memanggil, dan input kunci hash dari argumen fungsi. Dengan demikian metode ini dapat memanggil fungsi dengan nilai-nilai yang diperlukan dari peta hash, tanpa menyediakan akses baca publik ke nilai-nilai (dengan demikian sebagaimana diperlukan, menolak fungsi kemampuan untuk membaca nilai dari lingkungan).

Tetapi jika kunci hash ini string atau nilai-nilai hash integer, mengetik statis jenis hash peta elemen subsumes ke Setiap atau AnyRef (hash kode peta tidak ditampilkan di bawah), dan dengan demikian run-time ketidakcocokan bisa terjadi, yaitu akan mungkin untuk menempatkan semua jenis nilai dalam peta hash untuk kunci hash yang diberikan.

trait Env {
...
  def callit[A](func: Env => Any => A, arg1key: String): A
  def callit[A](func: Env => Any => Any => A, arg1key: String, arg2key: String): A
}

Meskipun saya tidak menguji yang berikut ini, secara teori saya bisa mendapatkan kunci hash dari nama kelas saat mempekerjakan runtime classOf, jadi kunci hash adalah nama kelas, bukan string (menggunakan backticks Scala untuk menanamkan string dalam nama kelas).

trait DependentHashKey {
  type ValueType
}
trait `the hash key string` extends DependentHashKey {
  type ValueType <: SomeType
}

Jadi keamanan tipe statis tercapai.

def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A
Shelby Moore III
sumber
Ketika kita perlu melewati kunci argumen di dalam nilai tunggal, saya tidak menguji, tetapi anggap kita dapat menggunakan Tuple, misalnya untuk argumen 2 kelebihan def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A. Kami tidak akan menggunakan kumpulan kunci argumen, karena tipe elemen akan dimasukkan (tidak diketahui pada saat kompilasi) dalam jenis koleksi.
Shelby Moore III
"pengetikan statis tipe elemen peta hash tunduk ke Any atau AnyRef" - Saya tidak mengikuti. Ketika Anda mengatakan tipe elemen yang Anda maksudkan tipe kunci atau tipe nilai (yaitu argumen tipe pertama atau kedua ke HashMap)? Dan mengapa itu digabung?
Robin Green
@RobinGreen Jenis nilai dalam tabel hash. Setelah itu, digolongkan karena Anda tidak dapat memasukkan lebih dari satu jenis dalam koleksi di Scala kecuali Anda berlangganan tipe supertype umum mereka, karena Scala tidak memiliki jenis gabungan (disjungsi). Lihat T&J saya tentang subsubsensi di Scala.
Shelby Moore III