Pertimbangkan Functor
kelas tipe di Haskell, di mana f
variabel 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 f
dari a
menjadi b
, tetapi membiarkannya f
apa adanya. Jadi jika Anda menggunakan fmap
lebih 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 Functor
abstraksi 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 map
metodenya mengembalikan jenis koleksi yang berbeda, atau bahkan sesuatu yang lain yang sama sekali bukan koleksi tetapi masih a Functor
. Selain itu, ketika Anda menggunakan map
metode 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
map
selalu mengembalikan Functor
subkelas yang sama dengan penerima.
- Oleh karena itu, tidak ada cara aman tipe statis untuk memanggil
Functor
metode 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 Functor
yang 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 Functor
dari map
metode, tetapi karena tidak ada batasan berapa banyak Functor
implementasi 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 Functor
kendala 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 FB
untuk memiliki yang sama F
dengan FA
—sehingga ketika Anda mendeklarasikan sebuah tipe List<A> implements Functor<List<A>, A>
, map
metode 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 F
akan dipakai untuk jenis yang tidak diparameterisasi seperti hanya List
atau 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 T
merupakan variabel tipe, Anda dapat menulis T
dan 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 f
atau myList |> Seq.map f |> Seq.toList
tipe yang lebih tinggi memungkinkan Anda untuk hanya menulis myList |> map f
dan 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 Seq
tetap dan kemudian saya dapat mengonversi ke apa pun yang saya inginkan sesudahnya.
Ada banyak bahasa yang menggeneralisasi gagasan map
fungsi 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, Functor
kelasnya lebih umum dari itu; itu tidak terkait dengan gagasan tentang urutan. Anda dapat menerapkan fmap
untuk jenis yang tidak memiliki pemetaan yang baik untuk urutan, seperti IO
tindakan, 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 fmap
mempertahankan jenisnya — karena segera setelah Anda mendapatkan map
operasi yang menghasilkan jenis hasil yang berbeda, akan jauh lebih sulit untuk membuat jaminan seperti ini.