Maaf, saya tidak terlalu paham matematika saya, jadi saya ingin tahu bagaimana cara mengucapkan fungsi di kelas tipe Aplikatif
Mengetahui matematika Anda, atau tidak, sebagian besar tidak relevan di sini, menurut saya. Seperti yang mungkin Anda sadari, Haskell meminjam beberapa bit terminologi dari berbagai bidang matematika abstrak, terutama Teori Kategori , dari mana kita mendapatkan functor dan monad. Penggunaan istilah-istilah ini di Haskell agak menyimpang dari definisi matematika formal, tetapi mereka biasanya cukup dekat untuk menjadi istilah deskriptif yang baik.
Kelas Applicative
tipe berada di antara Functor
dan Monad
, jadi orang akan mengharapkannya memiliki dasar matematika yang serupa. Dokumentasi untuk Control.Applicative
modul dimulai dengan:
Modul ini menjelaskan struktur perantara antara functor dan monad: modul ini menyediakan ekspresi dan urutan murni, tetapi tidak ada pengikatan. (Secara teknis, fungsi monoidal lemah yang kuat.)
Hmm.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
Tidak semenarik itu Monad
, menurut saya.
Apa yang pada dasarnya semua ini intinya adalah itu Applicative
tidak sesuai dengan konsep apa pun yang sangat menarik secara matematis, jadi tidak ada istilah siap pakai yang menangkap cara penggunaannya di Haskell. Jadi, kesampingkan matematika untuk saat ini.
Jika kita ingin tahu apa yang disebut (<*>)
, mungkin membantu untuk mengetahui apa artinya pada dasarnya.
Jadi apa dengan Applicative
, pula, dan mengapa tidak kita menyebutnya itu?
Apa yang Applicative
berjumlah dalam praktek adalah cara untuk mengangkat sewenang-wenang fungsi menjadi Functor
. Pertimbangkan kombinasi Maybe
(bisa dibilang yang paling sederhana non-sepele Functor
) dan Bool
(juga tipe data non-sepele yang paling sederhana).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
Fungsinya fmap
memungkinkan kita mengangkat not
dari bekerja Bool
ke mengerjakan Maybe Bool
. Tetapi bagaimana jika kita ingin mengangkat (&&)
?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Nah, itu bukan apa yang kita inginkan sama sekali ! Faktanya, itu sangat tidak berguna. Kita bisa mencoba menjadi pandai dan menyelinap Bool
masuk Maybe
melalui belakang ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... tapi itu tidak bagus. Untuk satu hal, itu salah. Untuk hal lain, itu jelek . Kami dapat terus mencoba, tetapi ternyata tidak ada cara untuk mengangkat fungsi dari beberapa argumen untuk bekerja secara sembaranganFunctor
. Menyebalkan!
Di sisi lain, kita bisa melakukannya dengan mudah jika kita menggunakan Maybe
's Monad
contoh:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Nah, itu sangat merepotkan hanya untuk menerjemahkan fungsi sederhana - itulah sebabnya Control.Monad
menyediakan fungsi untuk melakukannya secara otomatis , liftM2
. Angka 2 dalam namanya mengacu pada fakta bahwa ia bekerja pada fungsi tepat dua argumen; fungsi serupa ada untuk 3, 4, dan 5 fungsi argumen. Fungsi-fungsi ini lebih baik , tetapi tidak sempurna, dan menentukan jumlah argumen itu jelek dan kikuk.
Yang membawa kita ke makalah yang memperkenalkan kelas tipe Aplikatif . Di dalamnya, pada dasarnya penulis melakukan dua pengamatan:
- Mengangkat fungsi multi-argumen menjadi a
Functor
adalah hal yang sangat wajar untuk dilakukan
- Melakukannya tidak memerlukan kemampuan penuh dari a
Monad
Aplikasi fungsi normal ditulis dengan penjajaran istilah yang sederhana, sehingga untuk membuat "aplikasi yang diangkat" sesederhana dan sealami mungkin, makalah ini memperkenalkan operator infix untuk menggantikan aplikasi, diangkat ke dalamFunctor
, dan kelas tipe untuk menyediakan apa yang dibutuhkan untuk itu .
Semuanya membawa kita ke poin berikut: (<*>)
hanya mewakili aplikasi fungsi - jadi mengapa mengucapkannya secara berbeda dari yang Anda lakukan pada "operator penjajaran" spasi kosong?
Tetapi jika itu tidak terlalu memuaskan, kita dapat mengamati bahwa Control.Monad
modul juga menyediakan fungsi yang melakukan hal yang sama untuk monad:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Dimana ap
, tentu saja, singkatan dari "melamar". Karena apapun Monad
bisa Applicative
, dan ap
hanya membutuhkan subset dari fitur yang ada pada yang terakhir, kita mungkin bisa mengatakan bahwa jika (<*>)
bukan operator, itu harus dipanggil ap
.
Kita juga bisa mendekati sesuatu dari arah lain. The Functor
operasi pengangkatan disebut fmap
karena itu generalisasi dari map
operasi pada daftar. Seperti apa fungsi pada daftar yang akan berfungsi (<*>)
? Ada apa yang ap
ada di daftar, tentu saja, tapi itu tidak terlalu berguna.
Faktanya, mungkin ada interpretasi yang lebih alami untuk daftar. Apa yang terlintas dalam pikiran saat Anda melihat tanda tangan tipe berikut?
listApply :: [a -> b] -> [a] -> [b]
Ada sesuatu yang begitu menggoda tentang gagasan melapisi daftar secara paralel, menerapkan setiap fungsi di bagian pertama ke elemen yang sesuai di kedua. Sayangnya bagi teman lama kita Monad
, operasi sederhana ini melanggar hukum monad jika panjang daftarnya berbeda. Tapi itu membuat denda Applicative
, dalam hal ini (<*>)
menjadi cara merangkai versi umum zipWith
, jadi mungkin kita bisa membayangkan menyebutnya fzipWith
?
Ide membuat ritsleting ini benar-benar memberi kita lingkaran penuh. Ingat hal matematika sebelumnya, tentang fungsi monoid? Seperti namanya, berikut adalah cara untuk menggabungkan struktur monoid dan fungsi, keduanya adalah kelas tipe Haskell yang sudah dikenal:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Akan terlihat seperti apa jika Anda memasukkannya ke dalam kotak dan menggoyangnya sedikit? Dari Functor
kita akan menjaga gagasan tentang struktur tidak tergantung pada parameter tipenya , dan dari Monoid
kita akan mempertahankan bentuk fungsi secara keseluruhan:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
Kami tidak ingin berasumsi bahwa ada cara untuk membuat yang benar-benar "kosong" Functor
, dan kami tidak dapat membayangkan nilai dari sembarang tipe, jadi kami akan memperbaiki tipe mfEmpty
sebagai f ()
.
Kami juga tidak ingin memaksa mfAppend
untuk membutuhkan parameter tipe yang konsisten, jadi sekarang kami memiliki ini:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
Untuk apa jenis hasil tersebut mfAppend
? Kami memiliki dua tipe arbitrer yang tidak kami ketahui, jadi kami tidak memiliki banyak pilihan. Hal yang paling masuk akal adalah menyimpan keduanya:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
Pada titik mana mfAppend
sekarang jelas merupakan versi umum dari zip
daftar, dan kami dapat merekonstruksi Applicative
dengan mudah:
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
Ini juga menunjukkan kepada kita yang pure
terkait dengan elemen identitas a Monoid
, jadi nama bagus lainnya untuk itu mungkin apa pun yang menyarankan nilai unit, operasi nol, atau semacamnya.
Itu sangat panjang, jadi untuk meringkas:
(<*>)
hanyalah sebuah aplikasi fungsi yang dimodifikasi, jadi Anda dapat membacanya sebagai "ap" atau "apply", atau elide seluruhnya seperti yang Anda lakukan pada aplikasi fungsi normal.
(<*>)
juga secara kasar menggeneralisasi zipWith
daftar, sehingga Anda dapat membacanya sebagai "fungsi zip dengan", mirip dengan membaca fmap
sebagai "memetakan fungsi dengan".
Yang pertama lebih dekat dengan maksud dari Applicative
kelas tipe - seperti namanya - jadi itulah yang saya rekomendasikan.
Faktanya, saya mendorong penggunaan liberal, dan non-pengucapan, dari semua operator aplikasi yang diangkat :
(<$>)
, yang mengangkat fungsi argumen tunggal menjadi a Functor
(<*>)
, yang menghubungkan fungsi multi-argumen melalui Applicative
(=<<)
, yang mengikat fungsi yang memasukkan a Monad
ke komputasi yang sudah ada
Ketiganya, pada intinya, hanya aplikasi fungsi biasa, dibumbui sedikit.