Pertimbangkan Functorkelas tipe di Haskell, di mana fvariabel tipe tipe lebih tinggi:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Apa yang dikatakan oleh tanda tangan tipe ini adalah bahwa fmap mengubah parameter tipe fdari amenjadi b, tetapi membiarkannya fapa adanya. Jadi jika Anda menggunakan fmaplebih dari daftar Anda mendapatkan daftar, jika Anda menggunakannya di atas parser Anda mendapatkan parser, dan seterusnya. Dan ini adalah jaminan statis , waktu kompilasi.
Saya tidak tahu F #, tapi mari kita pertimbangkan apa yang terjadi jika kita mencoba mengekspresikan Functorabstraksi dalam bahasa seperti Java atau C #, dengan pewarisan dan generik, tetapi tidak ada generik yang lebih tinggi jenisnya. Percobaan pertama:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
Masalah dengan percobaan pertama ini adalah bahwa implementasi antarmuka diizinkan untuk mengembalikan kelas apa pun yang mengimplementasikan Functor. Seseorang dapat menulis FunnyList<A> implements Functor<A>yang mapmetodenya mengembalikan jenis koleksi yang berbeda, atau bahkan sesuatu yang lain yang sama sekali bukan koleksi tetapi masih a Functor. Selain itu, ketika Anda menggunakan mapmetode ini, Anda tidak dapat memanggil metode khusus subtipe apa pun pada hasil kecuali Anda menurunkannya ke tipe yang sebenarnya Anda harapkan. Jadi kita punya dua masalah:
- Sistem tipe tidak mengizinkan kita untuk mengekspresikan invarian yang
mapselalu mengembalikan Functorsubkelas yang sama dengan penerima.
- Oleh karena itu, tidak ada cara aman tipe statis untuk memanggil
Functormetode non- pada hasil map.
Ada cara lain yang lebih rumit yang dapat Anda coba, tetapi tidak ada yang benar-benar berhasil. Misalnya, Anda dapat mencoba menambah percobaan pertama dengan menentukan subtipe Functoryang membatasi jenis hasil:
interface Collection<A> extends Functor<A> {
Collection<B> map(Function<A, B> f);
}
interface List<A> extends Collection<A> {
List<B> map(Function<A, B> f);
}
interface Set<A> extends Collection<A> {
Set<B> map(Function<A, B> f);
}
interface Parser<A> extends Functor<A> {
Parser<B> map(Function<A, B> f);
}
Hal ini membantu untuk melarang pelaksana antarmuka yang lebih sempit tersebut mengembalikan jenis yang salah Functordari mapmetode, tetapi karena tidak ada batasan berapa banyak Functorimplementasi yang dapat Anda miliki, tidak ada batasan berapa banyak antarmuka sempit yang Anda perlukan.
( EDIT: Dan perhatikan bahwa ini hanya berfungsi karena Functor<B>muncul sebagai jenis hasil, sehingga antarmuka anak dapat mempersempitnya. Jadi AFAIK kami tidak dapat mempersempit kedua penggunaan Monad<B>dalam antarmuka berikut:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
Di Haskell, dengan variabel tipe peringkat lebih tinggi, ini adalah (>>=) :: Monad m => m a -> (a -> m b) -> m b.)
Namun percobaan lain adalah menggunakan generik rekursif untuk mencoba dan membuat antarmuka membatasi jenis hasil dari subtipe ke subtipe itu sendiri. Contoh mainan:
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
Tetapi teknik semacam ini (yang agak misterius bagi pengembang OOP run-of-the-mill Anda, heck ke pengembang fungsional run-of-the-mill Anda juga) masih tidak dapat mengungkapkan Functorkendala yang diinginkan juga:
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Masalahnya di sini adalah ini tidak membatasi FBuntuk memiliki yang sama Fdengan FA—sehingga ketika Anda mendeklarasikan sebuah tipe List<A> implements Functor<List<A>, A>, mapmetode tersebut masih dapat mengembalikan NotAList<B> implements Functor<NotAList<B>, B>.
Percobaan terakhir, di Java, menggunakan tipe mentah (kontainer tanpa parameter):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Di sini Fakan dipakai untuk jenis yang tidak diparameterisasi seperti hanya Listatau Map. Ini menjamin bahwa a FunctorStrategy<List>hanya dapat mengembalikan a List—tetapi Anda telah mengabaikan penggunaan variabel jenis untuk melacak jenis elemen daftar.
Inti dari masalah di sini adalah bahwa bahasa seperti Java dan C # tidak mengizinkan parameter tipe memiliki parameter. Di Java, jika Tmerupakan variabel tipe, Anda dapat menulis Tdan List<T>, tetapi tidak T<String>. Tipe yang lebih baik hati menghapus batasan ini, sehingga Anda bisa mendapatkan sesuatu seperti ini (tidak sepenuhnya dipikirkan):
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
<B> List<B> map(Function<A, B> f) {
}
}
Dan membahas bagian ini secara khusus:
(Saya pikir) Saya mengerti bahwa alih-alih myList |> List.map fatau myList |> Seq.map f |> Seq.toListtipe yang lebih tinggi memungkinkan Anda untuk hanya menulis myList |> map fdan itu akan mengembalikan a List. Itu bagus (dengan asumsi itu benar), tetapi tampaknya agak kecil? (Dan tidak bisakah itu dilakukan hanya dengan mengizinkan overloading fungsi?) Saya biasanya mengubahnya menjadi Seqtetap dan kemudian saya dapat mengonversi ke apa pun yang saya inginkan sesudahnya.
Ada banyak bahasa yang menggeneralisasi gagasan mapfungsi dengan cara ini, dengan memodelkannya seolah-olah, pada intinya, pemetaan adalah tentang urutan. Komentar Anda ini ada dalam semangat itu: jika Anda memiliki tipe yang mendukung konversi ke dan dari Seq, Anda mendapatkan operasi peta "gratis" dengan menggunakan kembali Seq.map.
Di Haskell, bagaimanapun, Functorkelasnya lebih umum dari itu; itu tidak terkait dengan gagasan tentang urutan. Anda dapat menerapkan fmapuntuk jenis yang tidak memiliki pemetaan yang baik untuk urutan, seperti IOtindakan, kombinator parser, fungsi, dll .:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g)
Konsep "pemetaan" sebenarnya tidak terikat pada urutan. Yang terbaik adalah memahami hukum functor:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Sangat informal:
- Hukum pertama mengatakan bahwa pemetaan dengan fungsi identitas / noop sama dengan tidak melakukan apa-apa.
- Hukum kedua mengatakan bahwa hasil apa pun yang dapat Anda hasilkan dengan pemetaan dua kali, Anda juga dapat menghasilkan dengan pemetaan sekali.
Inilah mengapa Anda ingin fmapmempertahankan jenisnya — karena segera setelah Anda mendapatkan mapoperasi yang menghasilkan jenis hasil yang berbeda, akan jauh lebih sulit untuk membuat jaminan seperti ini.