Mengapa Anda membutuhkan jenis yang lebih tinggi?

13

Beberapa bahasa mengizinkan kelas dan fungsi dengan parameter tipe (seperti di List<T>mana Tmungkin tipe arbitrer). Misalnya, Anda dapat memiliki fungsi seperti:

List<S> Function<S, T>(List<T> list)

Namun beberapa bahasa memungkinkan konsep ini diperluas satu tingkat lebih tinggi, memungkinkan Anda untuk memiliki fungsi dengan tanda tangan:

K<S> Function<K<_>, S, T>(K<T> arg)

Di mana K<_>itu sendiri adalah tipe seperti List<_>yang memiliki parameter tipe. "Tipe parsial" ini dikenal sebagai konstruktor tipe.

Pertanyaan saya adalah, mengapa Anda membutuhkan kemampuan ini? Masuk akal untuk memiliki tipe suka List<T>karena semuanya List<T>hampir persis sama, tetapi semua K<_>bisa sangat berbeda. Anda dapat memiliki Option<_>dan List<_>yang tidak memiliki fungsi sama sekali.

GregRos
sumber
7
Ada beberapa jawaban bagus di sini: stackoverflow.com/questions/21170493/…
itsbruce
3
@itsbruce Secara khusus, Functorcontoh dalam jawaban Luis Casillas cukup intuitif. Apa kesamaan List<T>dan Option<T>kesamaan? Jika Anda memberi saya salah satu dan fungsi T -> Ssaya bisa memberi Anda List<S>atau Option<S>. Kesamaan yang mereka miliki adalah bahwa Anda dapat mencoba untuk mendapatkan Tnilai dari keduanya.
Doval
@Doval: Bagaimana Anda melakukan yang pertama? Sejauh yang satu ini tertarik pada yang terakhir, saya akan berpikir bahwa bisa ditangani dengan memiliki kedua jenis menerapkan IReadableHolder<T>.
supercat
@supercat Saya menduga mereka akan harus melaksanakan IMappable<K<_>, T>dengan metode K<S> Map(Func<T, S> f), pelaksanaan sebagai IMappable<Option<_>, T>, IMappable<List<_>, T>. Jadi Anda harus membatasi K<T> : IMappable<K<_>, T>untuk memanfaatkannya.
GregRos
1
Casting bukanlah analogi yang tepat. Apa yang dilakukan dengan kelas tipe (atau konstruksi serupa) adalah Anda menunjukkan bagaimana tipe yang diberikan memuaskan tipe yang lebih tinggi. Ini biasanya melibatkan pendefinisian fungsi baru atau menunjukkan fungsi mana yang ada (atau metode jika bahasa OO-nya seperti Scala) dapat digunakan. Setelah ini dilakukan, semua fungsi yang didefinisikan untuk bekerja dengan tipe yang lebih tinggi semua akan bekerja dengan tipe itu. Tapi itu lebih dari sekedar antarmuka karena lebih dari satu set fungsi / metode tanda tangan yang sederhana didefinisikan. Saya kira saya harus pergi dan menulis jawaban untuk menunjukkan cara kerjanya.
itsbruce

Jawaban:

5

Karena tidak ada orang lain yang menjawab pertanyaan, saya pikir saya akan mencobanya sendiri. Saya harus sedikit filosofis.

Pemograman generik adalah semua tentang pengabstrakan pada tipe yang sama, tanpa kehilangan informasi tipe (yang terjadi dengan polimorfisme nilai berorientasi objek). Untuk melakukan ini, tipe-tipe tersebut harus harus berbagi semacam antarmuka (satu set operasi, bukan istilah OO) yang dapat Anda gunakan.

Dalam bahasa berorientasi objek, tipe memenuhi antarmuka berdasarkan kelas. Setiap kelas memiliki antarmuka sendiri, didefinisikan sebagai bagian dari tipenya. Karena semua kelas List<T>memiliki antarmuka yang sama, Anda dapat menulis kode yang berfungsi apa pun yang TAnda pilih. Cara lain untuk memaksakan antarmuka adalah batasan warisan, dan meskipun keduanya tampak berbeda, mereka agak mirip jika Anda memikirkannya.

Dalam sebagian besar bahasa berorientasi objek, List<>bukan jenis yang tepat. Tidak memiliki metode, dan karenanya tidak memiliki antarmuka. Hanya List<T>yang memiliki hal-hal ini. Pada dasarnya, dalam istilah yang lebih teknis, satu-satunya jenis yang dapat Anda abstraksi secara bermakna adalah yang memiliki jenis tersebut *. Untuk menggunakan tipe yang lebih tinggi di dunia berorientasi objek, Anda harus membatasi tipe frase dengan cara yang konsisten dengan batasan ini.

Misalnya, seperti yang disebutkan dalam komentar, kita dapat melihat Option<>dan List<>sebagai "dapat dipetakan", dalam arti bahwa jika Anda memiliki fungsi, Anda dapat mengonversikannya Option<T>menjadi Option<S>, atau List<T>menjadi List<S>. Mengingat bahwa kelas tidak dapat digunakan untuk abstrak ke tipe yang lebih tinggi secara langsung, kami membuat antarmuka:

IMappable<K<_>, T> where K<T> : IMappable<K<_>, T>

Dan kemudian kami mengimplementasikan antarmuka di keduanya List<T>dan Option<T>sebagai IMappable<List<_>, T>dan IMappable<Option<_>, T>masing - masing. Apa yang kami lakukan adalah menggunakan jenis yang lebih tinggi untuk menempatkan batasan pada jenis yang sebenarnya (yang tidak lebih tinggi) Option<T>dan List<T>. Ini adalah bagaimana hal itu dilakukan dalam Scala, meskipun tentu saja Scala memiliki fitur seperti ciri, variabel tipe, dan parameter implisit yang membuatnya lebih ekspresif.

Dalam bahasa lain, dimungkinkan untuk melakukan abstrak pada jenis yang lebih tinggi secara langsung. Di Haskell, salah satu otoritas tertinggi pada sistem tipe, kita dapat menggunakan kelas tipe untuk tipe apa pun, bahkan jika itu memiliki jenis yang lebih tinggi. Sebagai contoh,

class Mappable mp where
    map :: mp a -> mp b

Ini adalah kendala yang ditempatkan langsung pada tipe (tidak ditentukan) mpyang mengambil satu parameter tipe, dan mengharuskannya dikaitkan dengan fungsi mapyang mengubah suatu mp<a>menjadi mp<b>. Kita kemudian dapat menulis fungsi yang membatasi tipe yang lebih tinggi dengan Mappableseperti di bahasa berorientasi objek Anda bisa menempatkan batasan pewarisan. Yah, semacam itu.

Singkatnya, kemampuan Anda untuk menggunakan tipe yang lebih tinggi tergantung pada kemampuan Anda untuk membatasi atau menggunakannya sebagai bagian dari kendala tipe.

GregRos
sumber
Sudah terlalu sibuk berganti pekerjaan tetapi akhirnya akan menemukan waktu untuk menulis jawaban saya sendiri. Saya pikir Anda sudah terganggu oleh gagasan kendala. Salah satu aspek signifikan dari jenis yang lebih tinggi adalah bahwa mereka memungkinkan fungsi / perilaku didefinisikan yang dapat diterapkan pada jenis apa pun yang secara logis dapat ditunjukkan untuk memenuhi syarat. Jauh dari memaksakan batasan pada tipe yang ada, kelas tipe (satu aplikasi dari tipe yang lebih tinggi) menambah perilaku mereka.
itsbruce
Saya pikir ini masalah semantik. Kelas tipe mendefinisikan kelas tipe. Saat Anda mendefinisikan fungsi seperti (Mappable mp) => mp a -> mp b, Anda telah menempatkan batasan mpuntuk menjadi anggota kelas tipe Mappable. Saat Anda mendeklarasikan tipe seperti Optionmenjadi instance Mappable, Anda menambahkan perilaku ke tipe itu. Saya kira Anda bisa memanfaatkan perilaku itu secara lokal tanpa pernah membatasi tipe apa pun, tapi kemudian tidak ada bedanya dengan mendefinisikan fungsi biasa.
GregRos
Juga, saya cukup tipe kelas tidak berhubungan langsung dengan tipe yang lebih tinggi. Scala memiliki tipe yang lebih tinggi, tetapi bukan tipe kelas, dan Anda bisa membatasi kelas tipe untuk tipe dengan jenis *tanpa membuatnya tidak dapat digunakan. Memang benar bahwa kelas tipe sangat kuat ketika bekerja dengan tipe yang lebih tinggi.
GregRos
Scala memiliki kelas tipe. Mereka diimplementasikan dengan implisit. Implisit menyediakan yang setara dengan instance kelas tipe Haskell. Ini adalah implementasi yang lebih rapuh daripada Haskell karena tidak ada sintaks khusus untuk itu. Tapi itu selalu menjadi salah satu tujuan utama dari implisit Scala dan mereka melakukan pekerjaan itu.
itsbruce