Menurut pendapat saya yang sederhana, jawaban atas pertanyaan terkenal "Apa itu monad?" , terutama yang paling banyak dipilih, coba jelaskan apa itu monad tanpa menjelaskan dengan jelas mengapa monad benar-benar diperlukan . Bisakah mereka dijelaskan sebagai solusi untuk suatu masalah?
366
Jawaban:
Mengapa kita membutuhkan monad?
Lalu, kita punya masalah besar pertama. Ini adalah program:
f(x) = 2 * x
g(x,y) = x / y
Bagaimana kita bisa mengatakan apa yang harus dieksekusi terlebih dahulu ? Bagaimana kita bisa membentuk urutan fungsi yang diurutkan (yaitu program ) menggunakan tidak lebih dari fungsi ?
Solusi: menyusun fungsi . Jika Anda ingin pertama
g
dan kemudianf
, cukup tulisf(g(x,y))
. Dengan cara ini, "program" adalah fungsi juga:main = f(g(x,y))
. OKE, tapi ...Lebih banyak masalah: beberapa fungsi mungkin gagal (yaitu
g(2,0)
, bagi dengan 0). Kami tidak memiliki "pengecualian" di FP (pengecualian bukan fungsi). Bagaimana kita menyelesaikannya?Solusi: Mari kita izinkan fungsi mengembalikan dua jenis hal : alih-alih memiliki
g : Real,Real -> Real
(fungsi dari dua real menjadi nyata), mari kita izinkang : Real,Real -> Real | Nothing
(fungsi dari dua real menjadi (nyata atau tidak sama sekali)).Tetapi fungsi seharusnya (lebih sederhana) mengembalikan hanya satu hal .
Solusi: mari kita buat tipe data baru yang akan dikembalikan, " tipe tinju " yang melingkupi mungkin nyata atau tidak menjadi apa-apa. Karena itu, kita dapat memiliki
g : Real,Real -> Maybe Real
. OKE, tapi ...Apa yang terjadi sekarang
f(g(x,y))
?f
belum siap untuk mengkonsumsiMaybe Real
. Dan, kami tidak ingin mengubah setiap fungsi yang kami dapat hubungkan dengang
untuk mengkonsumsi aMaybe Real
.Solusi: mari kita memiliki fungsi khusus untuk "menghubungkan" / "menulis" / "tautan" fungsi . Dengan begitu, kita dapat, di belakang layar, mengadaptasi output dari satu fungsi untuk memberi makan fungsi berikut.
Dalam kasus kami:
g >>= f
(hubungkan / tulisg
kef
). Kami ingin>>=
mendapatkang
hasil, memeriksanya dan, kalau-kalauNothing
tidak meneleponf
dan kembaliNothing
; atau sebaliknya, ekstrak kotakReal
dan beri makanf
dengan itu. (Algoritma ini hanya implementasi>>=
untukMaybe
tipe). Perhatikan juga bahwa>>=
harus ditulis hanya sekali per "tipe tinju" (kotak yang berbeda, algoritma adaptasi yang berbeda).Banyak masalah lain muncul yang dapat dipecahkan dengan menggunakan pola yang sama: 1. Gunakan "kotak" untuk mengkodifikasi / menyimpan makna / nilai yang berbeda, dan memiliki fungsi seperti
g
itu mengembalikan "nilai kotak" tersebut. 2. Memiliki komposer / penghubungg >>= f
untuk membantu menghubungkang
keluaran kef
input, jadi kami tidak perlu mengubahnyaf
sama sekali.Masalah luar biasa yang dapat dipecahkan dengan menggunakan teknik ini adalah:
memiliki keadaan global yang setiap fungsi dalam urutan fungsi ("program") dapat membagikan: solusi
StateMonad
.Kami tidak suka "fungsi tidak murni": fungsi yang menghasilkan output berbeda untuk input yang sama . Oleh karena itu, mari tandai fungsi-fungsi tersebut, membuatnya untuk mengembalikan nilai yang ditandai / kotak:
IO
monad.Kebahagiaan total!
sumber
IO
monad hanyalah satu lagi masalah dalam daftarIO
(poin 7). Di sisi lainIO
hanya muncul sekali dan pada akhirnya, jadi, jangan mengerti "sebagian besar waktu Anda berbicara ... tentang IO".Either
). Jawaban terbanyak adalah tentang "Mengapa kita membutuhkan functors?".g >>= f
untuk membantu menghubungkang
output kef
input, jadi kami tidak perlu mengubahnyaf
sama sekali." ini sama sekali tidak benar . Sebelumnya, dalamf(g(x,y))
,f
bisa menghasilkan apa pun. Itu bisa sajaf:: Real -> String
. Dengan "komposisi monadik" itu harus diubah untuk menghasilkanMaybe String
, atau jenis tidak cocok. Apalagi,>>=
itu sendiri tidak cocok !! Itu>=>
yang melakukan komposisi ini, bukan>>=
. Lihat diskusi dengan dfeuer di bawah jawaban Carl.Jawabannya tentu saja, "Kami tidak" . Seperti halnya semua abstraksi, itu tidak perlu.
Haskell tidak membutuhkan abstraksi monad. Tidak perlu melakukan IO dalam bahasa murni. The
IO
jenis menangani itu baik-baik saja dengan sendirinya. Yang ada desugaring monadik darido
blok bisa diganti dengan desugaring untukbindIO
,returnIO
danfailIO
sebagaimana didefinisikan dalamGHC.Base
modul. (Ini bukan modul yang terdokumentasi tentang peretasan, jadi saya harus menunjuk sumbernya untuk dokumentasi.) Jadi tidak, tidak perlu abstraksi monad.Jadi jika tidak diperlukan, mengapa itu ada? Karena itu ditemukan bahwa banyak pola komputasi membentuk struktur monadik. Abstraksi suatu struktur memungkinkan penulisan kode yang berfungsi di semua contoh struktur itu. Untuk membuatnya lebih ringkas - penggunaan kembali kode.
Dalam bahasa fungsional, alat paling kuat yang ditemukan untuk penggunaan kembali kode adalah komposisi fungsi.
(.) :: (b -> c) -> (a -> b) -> (a -> c)
Operator lama yang baik sangat kuat. Ini membuatnya mudah untuk menulis fungsi-fungsi kecil dan merekatkannya bersama-sama dengan overhead sintaksis atau semantik yang minimal.Tetapi ada kasus-kasus ketika tipe tidak bekerja dengan benar. Apa yang Anda lakukan ketika Anda punya
foo :: (b -> Maybe c)
danbar :: (a -> Maybe b)
?foo . bar
tidak mengetik centang, karenab
danMaybe b
bukan tipe yang sama.Tapi ... itu hampir benar. Anda hanya ingin sedikit kelonggaran. Anda ingin dapat memperlakukan
Maybe b
seolah-olah pada dasarnyab
. Itu ide yang buruk untuk hanya memperlakukan mereka sebagai tipe yang sama. Itu kurang lebih sama dengan null pointer, yang oleh Tony Hoare dikenal sebagai kesalahan miliaran dolar . Jadi jika Anda tidak dapat memperlakukan mereka sebagai jenis yang sama, mungkin Anda dapat menemukan cara untuk memperpanjang mekanisme komposisi yang(.)
disediakan.Dalam hal ini, penting untuk benar-benar memeriksa teori yang mendasarinya
(.)
. Untungnya, seseorang telah melakukan ini untuk kita. Ternyata kombinasi dari(.)
danid
membentuk konstruk matematika dikenal sebagai kategori . Tetapi ada cara lain untuk membentuk kategori. Kategori Kleisli, misalnya, memungkinkan objek yang dikomposisi sedikit diperbesar. Kategori Kleisli untukMaybe
terdiri dari(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
danid :: a -> Maybe a
. Artinya, objek dalam kategori menambah(->)
denganMaybe
, jadi(a -> b)
menjadi(a -> Maybe b)
.Dan tiba-tiba, kami telah memperluas kekuatan komposisi untuk hal-hal yang
(.)
operasi tradisional tidak berhasil. Ini adalah sumber kekuatan abstraksi baru. Kategori Kleisli berfungsi dengan lebih dari jenisMaybe
. Mereka bekerja dengan setiap jenis yang dapat mengumpulkan kategori yang tepat, mematuhi hukum kategori.id . f
=f
f . id
=f
f . (g . h)
=(f . g) . h
Selama Anda dapat membuktikan bahwa tipe Anda mematuhi ketiga undang-undang tersebut, Anda dapat mengubahnya menjadi kategori Kleisli. Dan apa masalahnya tentang itu? Nah, ternyata monad adalah hal yang persis sama dengan kategori Kleisli.
Monad
'sreturn
adalah sama dengan Kleisliid
.Monad
's(>>=)
tidak identik dengan Kleisli(.)
, tapi ternyata menjadi sangat mudah untuk menulis masing-masing dalam hal yang lain. Dan hukum kategori sama dengan hukum monad, ketika Anda menerjemahkannya di antara perbedaan(>>=)
dan(.)
.Jadi mengapa harus melalui semua gangguan ini? Mengapa
Monad
abstraksi dalam bahasa ini? Seperti yang saya singgung di atas, ini memungkinkan penggunaan kembali kode. Bahkan memungkinkan kode digunakan kembali di sepanjang dua dimensi yang berbeda.Dimensi pertama penggunaan kembali kode datang langsung dari kehadiran abstraksi. Anda dapat menulis kode yang berfungsi di semua contoh abstraksi. Ada seluruh paket monad-loop yang terdiri dari loop yang bekerja dengan instance apa pun
Monad
.Dimensi kedua tidak langsung, tetapi mengikuti dari keberadaan komposisi. Ketika komposisi mudah, adalah wajar untuk menulis kode dalam potongan kecil yang dapat digunakan kembali. Ini adalah cara yang sama dengan meminta
(.)
operator untuk fungsi mendorong penulisan fungsi yang kecil dan dapat digunakan kembali.Jadi mengapa abstraksi itu ada? Karena terbukti sebagai alat yang memungkinkan lebih banyak komposisi dalam kode, menghasilkan penciptaan kode yang dapat digunakan kembali dan mendorong penciptaan kode yang lebih dapat digunakan kembali. Penggunaan kembali kode adalah salah satu grails suci pemrograman. Abstraksi monad ada karena menggerakkan kita sedikit ke arah cawan suci itu.
sumber
newtype Kleisli m a b = Kleisli (a -> m b)
,. Kategori Kleisli adalah fungsi di mana tipe pengembalian kategori (b
dalam hal ini) adalah argumen untuk konstruktor tipem
. IffKleisli m
membentuk kategori,m
adalah Monad.Kleisli m
tampaknya membentuk kategori yang objeknya adalah tipe Haskell dan sedemikian rupa sehingga panah daria
keb
adalah fungsi daria
kem b
, denganid = return
dan(.) = (<=<)
. Apakah itu benar, atau apakah saya mencampur berbagai tingkat hal atau sesuatu?a
danb
, tetapi mereka bukan fungsi sederhana. Mereka dihiasi dengan tambahanm
nilai pengembalian fungsi.Benjamin Pierce berkata dalam TAPL
Karena itulah bahasa yang dilengkapi dengan sistem tipe yang kuat lebih ekspresif daripada bahasa yang diketik dengan buruk. Anda dapat berpikir tentang monad dengan cara yang sama.
Sebagai @Carl dan sigfpe point, Anda dapat melengkapi datatype dengan semua operasi yang Anda inginkan tanpa menggunakan monad, typeclasses atau apa pun hal abstrak lainnya. Namun monad memungkinkan Anda tidak hanya untuk menulis kode yang dapat digunakan kembali, tetapi juga untuk abstrak semua detail yang berlebihan.
Sebagai contoh, katakanlah kita ingin memfilter daftar. Cara paling sederhana adalah dengan menggunakan
filter
fungsifilter (> 3) [1..10]
:, yang sama dengan[4,5,6,7,8,9,10]
.Versi yang sedikit lebih rumit
filter
, yang juga meneruskan akumulator dari kiri ke kanan, adalahUntuk mendapatkan semuanya
i
, seperti itui <= 10, sum [1..i] > 4, sum [1..i] < 25
, kita bisa menulisyang sama dengan
[3,4,5,6]
.Atau kita dapat mendefinisikan kembali
nub
fungsi, yang menghapus elemen duplikat dari daftar, dalam halfilterAccum
:nub' [1,2,4,5,4,3,1,8,9,4]
sama dengan[1,2,4,5,3,8,9]
. Daftar dilewatkan sebagai akumulator di sini. Kode berfungsi, karena dimungkinkan untuk meninggalkan daftar monad, sehingga seluruh perhitungan tetap murni (notElem
sebenarnya tidak digunakan>>=
, tetapi bisa). Namun tidak mungkin untuk meninggalkan mono IO dengan aman (yaitu Anda tidak dapat menjalankan aksi IO dan mengembalikan nilai murni - nilai selalu akan terbungkus dalam mono IO). Contoh lain adalah array yang dapat diubah: setelah Anda meninggalkan ST monad, tempat array yang dapat diubah tinggal, Anda tidak dapat memperbarui array dalam waktu yang konstan lagi. Jadi kita membutuhkan pemfilteran monadik dariControl.Monad
modul:filterM
mengeksekusi aksi monadik untuk semua elemen dari daftar, menghasilkan elemen, untuk mana tindakan monadik kembaliTrue
.Contoh pemfilteran dengan larik:
mencetak
[1,2,4,5,3,8,9]
seperti yang diharapkan.Dan versi dengan IO monad, yang menanyakan elemen apa yang harus dikembalikan:
Misalnya
Dan sebagai ilustrasi terakhir,
filterAccum
dapat didefinisikan dalam halfilterM
:dengan
StateT
monad, yang digunakan di bawah tenda, menjadi hanya tipe data biasa.Contoh ini menggambarkan, bahwa monad tidak hanya memungkinkan Anda untuk abstrak konteks komputasi dan menulis kode yang dapat digunakan kembali bersih (karena kompabilitas monads, seperti @Carl menjelaskan), tetapi juga untuk memperlakukan tipe data yang ditentukan pengguna dan primitif bawaan secara seragam.
sumber
Saya tidak berpikir
IO
harus dilihat sebagai monad yang sangat luar biasa, tapi itu pasti salah satu yang lebih mengejutkan untuk pemula, jadi saya akan menggunakannya untuk penjelasan saya.Naif membangun sistem IO untuk Haskell
Sistem IO yang paling sederhana yang dapat dibayangkan untuk bahasa yang murni fungsional (dan sebenarnya yang dimulai dengan Haskell) adalah ini:
Dengan kemalasan, tanda tangan sederhana itu cukup untuk benar-benar membangun program terminal interaktif - meskipun sangat terbatas. Yang paling membuat frustrasi adalah kita hanya bisa menampilkan teks. Bagaimana jika kita menambahkan beberapa kemungkinan hasil yang lebih menarik?
lucu, tapi tentu saja "output alteratif" yang jauh lebih realistis adalah menulis ke file . Tetapi kemudian Anda juga ingin beberapa cara untuk membaca dari file. Ada kesempatan?
Nah, ketika kita mengambil
main₁
program kita dan hanya pipa file ke proses (menggunakan fasilitas sistem operasi), pada dasarnya kita telah menerapkan membaca file. Jika kita dapat memicu pembacaan file dari dalam bahasa Haskell ...Ini akan menggunakan "program interaktif"
String->[Output]
, memberinya string yang diperoleh dari file, dan menghasilkan program non-interaktif yang hanya mengeksekusi yang diberikan.Ada satu masalah di sini: kami tidak benar-benar memiliki gagasan tentang kapan file dibaca. The
[Output]
daftar yakin memberikan perintah bagus untuk output , tapi kami tidak mendapatkan pesanan untuk saat input akan dilakukan.Solusi: buat input-event juga item dalam daftar hal yang harus dilakukan.
Ok, sekarang Anda dapat menemukan ketidakseimbangan: Anda dapat membaca file dan membuat output bergantung padanya, tetapi Anda tidak dapat menggunakan konten file untuk memutuskan untuk misalnya juga membaca file lain. Solusi yang jelas: jadikan hasil dari input-event juga sesuatu yang bertipe
IO
, bukan hanyaOutput
. Itu pasti termasuk output teks sederhana, tetapi juga memungkinkan membaca file tambahan dll.Itu sekarang benar-benar memungkinkan Anda untuk mengekspresikan operasi file apa pun yang Anda inginkan dalam suatu program (walaupun mungkin tidak dengan kinerja yang baik), tetapi agak rumit:
main₃
menghasilkan daftar seluruh tindakan. Mengapa kita tidak menggunakan tanda tangan saja:: IO₁
, yang memiliki ini sebagai kasus khusus?Daftar tidak benar-benar memberikan gambaran umum aliran program lagi: sebagian besar perhitungan selanjutnya hanya akan "diumumkan" sebagai hasil dari beberapa operasi input. Jadi kita mungkin juga membuang struktur daftar, dan cukup kontra "dan kemudian lakukan" untuk setiap operasi output.
Lumayan!
Jadi apa hubungan semua ini dengan monad?
Dalam praktiknya, Anda tidak ingin menggunakan konstruktor polos untuk mendefinisikan semua program Anda. Perlu ada beberapa konstruktor fundamental yang baik, namun untuk sebagian besar hal tingkat tinggi kami ingin menulis fungsi dengan beberapa tanda tangan tingkat tinggi yang bagus. Ternyata sebagian besar dari ini akan terlihat sangat mirip: menerima semacam nilai yang diketik secara bermakna, dan menghasilkan tindakan IO sebagai hasilnya.
Jelas ada pola di sini, dan sebaiknya kita menuliskannya sebagai
Sekarang mulai terlihat akrab, tetapi kita masih hanya berurusan dengan fungsi polos yang disamarkan di bawah tenda, dan itu berisiko: setiap "nilai-tindakan" memiliki tanggung jawab untuk benar-benar meneruskan tindakan yang dihasilkan dari setiap fungsi yang terkandung (selain itu) aliran kontrol dari seluruh program mudah terganggu oleh satu tindakan tidak sopan di tengah). Lebih baik kita membuat persyaratan itu eksplisit. Ya, ternyata itu adalah undang-undang monad , meskipun saya tidak yakin kita dapat benar-benar merumuskannya tanpa operator bind / join standar.
Bagaimanapun, kami sekarang telah mencapai formulasi IO yang memiliki instance monad yang tepat:
Jelas ini bukan implementasi IO yang efisien, tetapi pada prinsipnya dapat digunakan.
sumber
IO3 a ≡ Cont IO2 a
. Tapi yang saya maksudkan komentar itu lebih sebagai anggukan kepada mereka yang sudah tahu kelanjutan monad, karena itu tidak memiliki reputasi sebagai ramah pemula.Monads hanyalah kerangka kerja yang nyaman untuk memecahkan kelas masalah berulang. Pertama, monads harus berfungsi (yaitu harus mendukung pemetaan tanpa melihat elemen (atau tipenya)), mereka juga harus membawa operasi pengikatan (atau rantai) dan cara untuk menciptakan nilai monadik dari tipe elemen (
return
). Akhirnya,bind
danreturn
harus memenuhi dua persamaan (identitas kiri dan kanan), juga disebut hukum monad. (Atau seseorang dapat mendefinisikan monads untuk memilikiflattening operation
alih - alih mengikat.)The Daftar monad umumnya digunakan untuk menangani non-determinisme. Operasi bind memilih salah satu elemen dari daftar (secara intuitif semuanya dalam dunia paralel ), memungkinkan programmer untuk melakukan perhitungan dengan mereka, dan kemudian menggabungkan hasil di semua dunia ke daftar tunggal (dengan menggabungkan, atau meratakan, daftar bersarang ). Inilah cara seseorang mendefinisikan fungsi permutasi dalam kerangka monadik Haskell:
Berikut adalah contoh repl sesi:
Perlu dicatat bahwa daftar monad sama sekali bukan sisi yang mempengaruhi perhitungan. Struktur matematika menjadi monad (yaitu sesuai dengan antarmuka dan hukum yang disebutkan di atas) tidak menyiratkan efek samping, meskipun fenomena efek samping sering kali cocok dengan kerangka kerja monadik.
sumber
Monads pada dasarnya berfungsi untuk menyusun fungsi bersama dalam sebuah rantai. Titik.
Sekarang cara mereka menyusun berbeda di monad yang ada, sehingga menghasilkan perilaku yang berbeda (misalnya, untuk mensimulasikan keadaan yang bisa berubah di negara monad).
Kebingungan tentang monad adalah bahwa menjadi begitu umum, yaitu, mekanisme untuk menyusun fungsi, mereka dapat digunakan untuk banyak hal, sehingga membuat orang percaya bahwa monad adalah tentang keadaan, tentang IO, dll, ketika mereka hanya tentang "menyusun fungsi ".
Sekarang, satu hal yang menarik tentang monad, adalah bahwa hasil komposisi selalu bertipe "M a", yaitu nilai di dalam amplop yang ditandai dengan "M". Fitur ini kebetulan sangat bagus untuk diterapkan, misalnya, pemisahan yang jelas antara kode murni dari kode tidak murni: nyatakan semua tindakan tidak murni sebagai fungsi dari tipe "IO a" dan tidak memberikan fungsi, saat mendefinisikan monad IO, untuk mengambil " a "nilai dari dalam" IO a ". Hasilnya adalah bahwa tidak ada fungsi yang dapat murni dan pada saat yang sama mengambil nilai dari "IO a", karena tidak ada cara untuk mengambil nilai seperti itu sambil tetap murni (fungsi harus berada di dalam monad "IO" untuk menggunakan nilai tersebut). (CATATAN: well, tidak ada yang sempurna, jadi "jaket pengikat IO" dapat dipecahkan menggunakan "unsafePerformIO: IO a -> a"
sumber
Anda perlu monad jika Anda memiliki konstruktor tipe dan fungsi yang mengembalikan nilai dari keluarga tipe itu . Akhirnya, Anda ingin menggabungkan fungsi-fungsi semacam ini bersama-sama . Inilah tiga elemen kunci untuk menjawab alasannya .
Biarkan saya uraikan. Anda memiliki
Int
,String
danReal
fungsi tipeInt -> String
,String -> Real
dan sebagainya. Anda dapat menggabungkan fungsi-fungsi ini dengan mudah, diakhiri denganInt -> Real
. Hidup itu baik.Kemudian, suatu hari, Anda perlu membuat baru keluarga dari jenis . Ini bisa jadi karena Anda perlu mempertimbangkan kemungkinan tidak mengembalikan nilai (
Maybe
), mengembalikan kesalahan (Either
), beberapa hasil (List
) dan sebagainya.Perhatikan bahwa itu
Maybe
adalah konstruktor tipe. Dibutuhkan jenis, sukaInt
dan mengembalikan jenis baruMaybe Int
. Hal pertama yang harus diingat, tidak ada konstruktor tipe, tidak ada monad.Tentu saja, Anda ingin menggunakan konstruktor tipe Anda dalam kode Anda, dan segera Anda berakhir dengan fungsi seperti
Int -> Maybe String
danString -> Maybe Float
. Sekarang, Anda tidak dapat dengan mudah menggabungkan fungsi Anda. Hidup sudah tidak baik lagi.Dan inilah saatnya para monad datang untuk menyelamatkan. Mereka memungkinkan Anda untuk menggabungkan fungsi semacam itu lagi. Anda hanya perlu mengubah komposisi . untuk > == .
sumber