Nama teknik untuk menyimpulkan argumen tipe dari parameter tipe?

9

Setup: Mari kita asumsikan kita memiliki tipe yang disebut Iteratoryang memiliki parameter tipe Element:

interface Iterator<Element> {}

Kemudian kami memiliki antarmuka Iterableyang memiliki satu metode yang akan mengembalikan Iterator.

// T has an upper bound of Iterator
interface Iterable<T: Iterator> {
    getIterator(): T
}

Masalah dengan Iteratormenjadi generik adalah bahwa kita harus menyediakannya dengan argumen tipe.

Satu ide untuk menyelesaikan ini adalah "menyimpulkan" jenis iterator. Kode pseudo berikut ini mengungkapkan ide bahwa ada variabel tipe Elementyang disimpulkan sebagai argumen tipe ke Iterator:

interface <Element> Iterable<T: Iterator<Element>> {
    getIterator(): T
}

Dan kemudian kita menggunakannya di suatu tempat seperti ini:

class Vec<Element> implements Iterable<VecIterator<Element>> {/*...*/}

Definisi ini Iterabletidak digunakan di Elementtempat lain dalam definisinya tetapi kasus penggunaan saya yang sebenarnya tidak. Fungsi-fungsi tertentu yang menggunakan Iterablejuga harus dapat membatasi parameter mereka untuk menerima Iterableyang hanya mengembalikan jenis iterator tertentu, seperti iterator dua arah, yang mengapa iterator yang dikembalikan adalah parameter bukan hanya tipe elemen.


Pertanyaan:

  • Apakah ada nama yang ditetapkan untuk variabel tipe disimpulkan ini? Bagaimana dengan teknik secara keseluruhan? Tidak mengetahui nomenklatur spesifik telah membuatnya sulit untuk mencari contoh-contoh ini di alam liar atau belajar tentang fitur-fitur khusus bahasa.
  • Tidak semua bahasa dengan obat generik memiliki teknik ini; apakah ada nama untuk teknik serupa dalam bahasa-bahasa ini?
Levi Morrison
sumber
1
Bisakah Anda menampilkan beberapa kode yang tidak bisa dikompilasi yang ingin Anda kompilasi? Saya pikir itu akan membuat pertanyaan menjadi lebih jelas.
Sweeper
1
Anda juga dapat memilih satu bahasa (atau mengatakan bahasa apa yang Anda gunakan, dan apa artinya sintaks). Jelas bukan, misalnya, C #. Ada banyak informasi tentang "ketik inferensi" yang tersedia di internet, tetapi saya tidak yakin itu berlaku di sini.
5
Saya menerapkan obat generik dalam suatu bahasa, tidak mencoba mendapatkan kode dalam satu bahasa apa pun untuk dikompilasi. Ini juga pertanyaan penamaan dan desain. Inilah mengapa ini agak agnostik. Tanpa mengetahui istilah-istilahnya, sulit untuk menemukan contoh dan dokumentasi dalam bahasa yang ada. Tentunya ini bukan masalah unik?
Levi Morrison

Jawaban:

2

Saya tidak tahu apakah ada istilah khusus untuk masalah ini, tetapi ada tiga kelas solusi umum:

  • hindari jenis beton yang mendukung pengiriman dinamis
  • memungkinkan parameter tipe placeholder dalam batasan tipe
  • hindari parameter tipe dengan menggunakan tipe / keluarga tipe terkait

Dan tentu saja solusi default: terus mengeja semua parameter tersebut.

Hindari jenis beton.

Anda telah mendefinisikan Iterableantarmuka sebagai:

interface <Element> Iterable<T: Iterator<Element>> {
    getIterator(): T
}

Ini memberi pengguna antarmuka daya maksimum karena mereka mendapatkan jenis beton yang tepat Tdari iterator. Ini juga memungkinkan kompiler untuk menerapkan lebih banyak optimasi seperti inlining.

Namun, jika Iterator<E>antarmuka yang dikirim secara dinamis maka mengetahui jenis konkret tidak diperlukan. Ini misalnya solusi yang digunakan Java. Antarmuka akan ditulis sebagai:

interface Iterable<Element> {
    getIterator(): Iterator<Element>
}

Variasi yang menarik dari ini adalah impl Traitsintaks Rust yang memungkinkan Anda mendeklarasikan fungsi dengan tipe pengembalian abstrak, tetapi mengetahui bahwa tipe konkret akan diketahui di situs panggilan (sehingga memungkinkan optimasi). Ini berperilaku mirip dengan parameter tipe implisit.

Izinkan parameter jenis placeholder.

The Iterableantarmuka tidak perlu tahu tentang jenis elemen, jadi mungkin akan mungkin untuk menulis ini sebagai:

interface Iterable<T: Iterator<_>> {
    getIterator(): T
}

Di mana T: Iterator<_>menyatakan kendala "T adalah iterator apa pun, terlepas dari jenis elemen". Lebih tepatnya, kita dapat menyatakan ini sebagai: "ada beberapa jenis Elementsehingga Tmerupakan Iterator<Element>", tanpa harus mengetahui jenis konkret untuk Element. Ini berarti bahwa tipe-ekspresi Iterator<_>tidak menggambarkan tipe aktual, dan hanya dapat digunakan sebagai batasan tipe.

Gunakan tipe keluarga / tipe terkait.

Misalnya dalam C ++, suatu tipe mungkin memiliki anggota tipe. Ini umum digunakan di seluruh perpustakaan standar, misalnya std::vector::value_type. Ini tidak benar-benar menyelesaikan masalah parameter tipe di semua skenario, tetapi karena suatu tipe dapat merujuk ke tipe lain, parameter tipe tunggal dapat menggambarkan seluruh keluarga jenis terkait.

Mari kita definisikan:

interface Iterator {
  type ElementType
  fn next(): ElementType
}

interface Iterable {
  type IteratorType: Iterator
  fn getIterator(): IteratorType
}

Kemudian:

class Vec<Element> implement Iterable {
  type IteratorType = VecIterator<Element>
  fn getIterator(): IteratorType { ... }
}

class VecIterator<T> implements Iterator {
  type ElementType = T
  fn next(): ElementType { ... }
}

Ini terlihat sangat fleksibel, tetapi perhatikan bahwa ini dapat membuatnya lebih sulit untuk mengekspresikan batasan tipe. Misal seperti yang tertulis Iterabletidak menerapkan tipe elemen iterator apa pun, dan kami mungkin ingin mendeklarasikannya interface Iterator<T>. Dan Anda sekarang berurusan dengan jenis kalkulus yang cukup kompleks. Sangat mudah untuk secara tidak sengaja membuat sistem semacam itu tidak dapat dipastikan (atau mungkin sudah?).

Perhatikan bahwa tipe terkait bisa sangat nyaman sebagai default untuk parameter tipe. Misalnya dengan asumsi bahwa Iterableantarmuka memerlukan parameter tipe terpisah untuk tipe elemen yang biasanya tetapi tidak selalu sama dengan tipe elemen iterator, dan bahwa kita memiliki parameter tipe placeholder, mungkin bisa dikatakan:

interface Iterable<T: Iterator<_>, Element = T::Element> {
  ...
}

Namun, itu hanya fitur ergonomi bahasa, dan tidak membuat bahasa lebih kuat.


Tipe sistemnya sulit, jadi bagus untuk melihat apa yang berfungsi dan tidak berfungsi dalam bahasa lain.

Misalnya pertimbangkan untuk membaca bab Ciri-Ciri Tingkat Lanjut dalam Rust Book, yang membahas tipe-tipe terkait. Tetapi perlu dicatat bahwa beberapa poin yang mendukung jenis terkait bukan generik hanya berlaku di sana karena bahasa tidak memiliki fitur subtipe dan setiap sifat hanya dapat diimplementasikan paling banyak sekali per jenis. Ie Rust traits bukan antarmuka seperti Java.

Sistem tipe menarik lainnya termasuk Haskell dengan berbagai ekstensi bahasa. Modul / fungsi OCaml adalah versi tipe keluarga yang relatif sederhana, tanpa secara langsung mencampurkannya dengan objek atau tipe parameter. Java terkenal karena keterbatasan dalam sistem tipenya, mis. Generik dengan penghapusan tipe, dan tidak ada generik atas tipe nilai. C # sangat mirip dengan Java tetapi berhasil menghindari sebagian besar keterbatasan ini, dengan biaya peningkatan kompleksitas implementasi. Scala mencoba mengintegrasikan generik gaya C # dengan kacamata jenis Haskell di atas platform Java. Templat sederhana C ++ yang menipu dipelajari dengan baik tetapi tidak seperti kebanyakan implementasi generik.

Ada baiknya juga melihat perpustakaan standar bahasa ini (terutama koleksi perpustakaan standar seperti daftar atau tabel hash) untuk melihat pola mana yang umum digunakan. Misalnya C ++ memiliki sistem kompleks dengan kemampuan iterator yang berbeda, dan Scala menyandikan kemampuan pengumpulan halus sebagai ciri. Antarmuka perpustakaan standar Java terkadang tidak sehat, misalnya Iterator#remove(), tetapi dapat menggunakan kelas bersarang sebagai jenis tipe terkait (misalnya Map.Entry).

amon
sumber