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 Iterable
antarmuka sebagai:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
Ini memberi pengguna antarmuka daya maksimum karena mereka mendapatkan jenis beton yang tepat T
dari 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 Trait
sintaks 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 Iterable
antarmuka 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 Element
sehingga T
merupakan 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 Iterable
tidak 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 Iterable
antarmuka 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
).