Sambil mempelajari Haskell, saya telah menghadapi banyak tutorial yang mencoba menjelaskan apa itu monad dan mengapa monad penting dalam Haskell. Masing-masing menggunakan analogi sehingga akan lebih mudah menangkap artinya. Pada akhirnya, saya memiliki 3 pandangan berbeda tentang apa itu monad:
Lihat 1: Monad sebagai label
Terkadang saya berpikir bahwa monad sebagai label untuk tipe tertentu. Misalnya, fungsi ketik:
myfunction :: IO Int
fungsi saya adalah fungsi yang setiap kali dilakukan akan menghasilkan nilai Int. Jenis hasilnya bukan Int tetapi IO Int. Jadi, IO adalah label dari nilai Int yang memperingatkan pengguna untuk mengetahui bahwa nilai Int adalah hasil dari proses di mana tindakan IO telah dibuat.
Akibatnya, nilai Int ini telah ditandai sebagai nilai yang berasal dari proses dengan IO sehingga nilai ini "kotor". Proses Anda tidak murni lagi.
Lihat 2: Monad sebagai ruang pribadi tempat hal-hal buruk dapat terjadi.
Dalam sistem di mana semua prosesnya murni dan ketat kadang-kadang Anda perlu memiliki efek samping. Jadi, monad hanyalah sedikit ruang yang memungkinkan Anda untuk melakukan efek samping yang buruk. Dalam ruang ini, Anda diizinkan untuk melarikan diri dari dunia murni, pergi najis, membuat proses Anda dan kemudian kembali dengan nilai.
Lihat 3: Monad dalam teori kategori
Ini adalah pandangan yang saya tidak sepenuhnya mengerti. Monad hanyalah sebuah fungsi untuk kategori atau sub-kategori yang sama. Misalnya, Anda memiliki nilai Int dan sebagai sub IO Int, yang merupakan nilai Int yang dihasilkan setelah proses IO.
Apakah pandangan ini benar? Mana yang lebih akurat?
Jawaban:
Tampilan # 1 dan # 2 secara umum salah.
* -> *
dapat berfungsi sebagai label, monad jauh lebih dari itu.IO
monad) perhitungan dalam monad tidak tidak murni. Mereka hanya mewakili perhitungan yang kami anggap memiliki efek samping, tetapi murni.Kedua kesalahpahaman ini berasal dari fokus pada
IO
monad, yang sebenarnya agak istimewa.Saya akan mencoba menguraikan sedikit tentang # 3, tanpa masuk ke dalam teori kategori jika memungkinkan.
Perhitungan standar
Semua perhitungan dalam bahasa pemrograman fungsional dapat dilihat sebagai fungsi dengan jenis sumber dan jenis target:
f :: a -> b
. Jika suatu fungsi memiliki lebih dari satu argumen, kita dapat mengonversinya menjadi fungsi satu argumen dengan memilah (lihat juga Haskell wiki ). Dan jika kita hanya memiliki nilaix :: a
(fungsi dengan 0 argumen), kita bisa mengubahnya menjadi fungsi yang mengambil argumen dari jenis unit :(\_ -> x) :: () -> a
.Kita dapat membangun program yang lebih kompleks dari yang lebih sederhana dengan menyusun fungsi-fungsi tersebut menggunakan
.
operator. Misalnya, jika kita milikif :: a -> b
dang :: b -> c
kita dapatkang . f :: a -> c
. Perhatikan bahwa ini juga berfungsi untuk nilai yang dikonversi: Jika kami memilikix :: a
dan mengonversinya menjadi representasi kami, kami mendapatkannyaf . ((\_ -> x) :: () -> a) :: () -> b
.Representasi ini memiliki beberapa sifat yang sangat penting, yaitu:
id :: a -> a
untuk setiap jenisa
. Ini adalah elemen identitas sehubungan dengan.
:f
sama denganf . id
dan untukid . f
..
bersifat asosiatif .Perhitungan monadik
Misalkan kita ingin memilih dan bekerja dengan beberapa kategori perhitungan khusus, yang hasilnya berisi lebih dari sekedar nilai pengembalian tunggal. Kami tidak ingin menentukan apa artinya "sesuatu yang lebih", kami ingin menjaga hal-hal yang bersifat umum. Cara paling umum untuk merepresentasikan "sesuatu yang lebih" adalah merepresentasikannya sebagai fungsi tipe - suatu
m
jenis* -> *
(yaitu mengkonversi satu tipe ke yang lain). Jadi untuk setiap kategori perhitungan yang ingin kami kerjakan, kami akan memiliki beberapa fungsi tipem :: * -> *
. (Dalam Haskell,m
adalah[]
,IO
,Maybe
, dll) dan kategori akan berisi semua fungsi dari jenisa -> m b
.Sekarang kami ingin bekerja dengan fungsi-fungsi dalam kategori seperti itu dengan cara yang sama seperti pada kasus dasar. Kami ingin dapat menyusun fungsi-fungsi ini, kami ingin komposisi menjadi asosiatif, dan kami ingin memiliki identitas. Kita butuh:
<=<
) yang menyusun fungsif :: a -> m b
dang :: b -> m c
menjadi sesuatu sebagaig <=< f :: a -> m c
. Dan, itu harus asosiatif.return
. Kami juga ingin ituf <=< return
samaf
dan sama denganreturn <=< f
.Apa pun
m :: * -> *
yang kami punya fungsi seperti itureturn
dan<=<
disebut monad . Hal ini memungkinkan kita untuk membuat perhitungan yang kompleks dari yang lebih sederhana, seperti pada kasus dasar, tetapi sekarang jenis nilai pengembalian diubah olehm
.(Sebenarnya, saya sedikit menyalahgunakan istilah kategori di sini. Dalam pengertian teori-kategori, kita dapat menyebut konstruksi kita sebagai kategori hanya setelah kita tahu itu mematuhi hukum-hukum ini.)
Monads di Haskell
Dalam Haskell (dan bahasa fungsional lainnya) kami sebagian besar bekerja dengan nilai, bukan dengan fungsi tipe
() -> a
. Jadi alih-alih mendefinisikan<=<
untuk setiap monad, kami mendefinisikan fungsi(>>=) :: m a -> (a -> m b) -> m b
. Definisi alternatif semacam itu setara, kita bisa ungkapkan>>=
menggunakan<=<
dan sebaliknya (coba sebagai latihan, atau lihat sumbernya ). Prinsipnya kurang jelas sekarang, tetapi tetap sama: hasil kami selalu tipem a
dan kami menyusun fungsi tipea -> m b
.Untuk setiap monad yang kita buat, kita tidak boleh lupa untuk memeriksanya
return
dan<=<
memiliki properti yang kami butuhkan: asosiatif dan identitas kiri / kanan. Dinyatakan menggunakanreturn
dan>>=
mereka disebut hukum monad .Contoh - daftar
Jika kita memilih
m
untuk menjadi[]
, kita mendapatkan kategori fungsi tipea -> [b]
. Fungsi tersebut mewakili perhitungan non-deterministik, yang hasilnya bisa satu atau lebih nilai, tetapi juga tidak ada nilai. Ini memunculkan apa yang disebut daftar monad . Komposisif :: a -> [b]
dang :: b -> [c]
berfungsi sebagai berikut:g <=< f :: a -> [c]
berarti menghitung semua hasil yang mungkin dari jenis[b]
, berlakug
untuk masing-masing, dan mengumpulkan semua hasil dalam satu daftar. Disajikan dalam Haskellatau menggunakan
>>=
Perhatikan bahwa dalam contoh ini, tipe-tipe kembalinya
[a]
sangat mungkin sehingga mereka tidak mengandung nilai tipe apa puna
. Memang, tidak ada persyaratan untuk monad sehingga tipe pengembalian harus memiliki nilai seperti itu. Beberapa monad selalu memiliki (sukaIO
atauState
), tetapi beberapa tidak, suka[]
atauMaybe
.IO Monad
Seperti yang saya sebutkan,
IO
monad agak istimewa. Nilai tipeIO a
berarti nilai tipe yanga
dibangun dengan berinteraksi dengan lingkungan program. Jadi (tidak seperti semua monad lainnya), kita tidak dapat menggambarkan nilai tipeIO a
menggunakan beberapa konstruksi murni. BerikutIO
ini hanyalah tag atau label yang membedakan komputasi yang berinteraksi dengan lingkungan. Ini (satu-satunya kasus) di mana tampilan # 1 dan # 2 benar.Untuk
IO
monad:f :: a -> IO b
dang :: b -> IO c
cara: Menghitungf
yang berinteraksi dengan lingkungan, dan kemudian menghitungg
yang menggunakan nilai dan menghitung hasil yang berinteraksi dengan lingkungan.return
cukup tambahkanIO
"tag" ke nilai (kami hanya "menghitung" hasilnya dengan menjaga lingkungan tetap utuh).Beberapa catatan:
m a
, tidak ada cara bagaimana "melarikan diri" dariIO
monad. Artinya adalah: Setelah perhitungan berinteraksi dengan lingkungan, Anda tidak dapat membuat perhitungan dari itu yang tidak.IO
monad. Inilah sebabnya mengapaIO
sering disebut sin bin programmer .getChar
harus memiliki tipe hasilIO something
.sumber
IO
tidak memiliki semantik khusus dari sudut pandang bahasa. Itu tidak istimewa, berperilaku seperti kode lainnya. Hanya implementasi pustaka runtime yang spesial. Juga, ada cara tujuan khusus untuk melarikan diri (unsafePerformIO
). Saya pikir ini penting karena orang sering menganggapIO
sebagai elemen bahasa khusus atau tag deklaratif. Bukan itu.coerce :: a -> b
yang mengubah dua jenis (dan crash program Anda dalam banyak kasus). Lihat contoh ini - Anda dapat mengubah bahkan suatu fungsi menjadiInt
dll.runST :: (forall s. GHC.ST.ST s a) -> a
Lihat 1: Monad sebagai label
"Akibatnya, nilai Int ini telah ditandai sebagai nilai yang berasal dari suatu proses dengan IO karena itu nilai ini" kotor "."
"IO Int" secara umum bukan nilai Int (meskipun mungkin dalam beberapa kasus seperti "return 3"). Ini adalah prosedur yang menghasilkan nilai Int. Eksekusi yang berbeda dari "prosedur" ini dapat menghasilkan nilai Int yang berbeda.
Monad m, adalah "bahasa pemrograman" tertanam (imperatif): dalam bahasa ini dimungkinkan untuk mendefinisikan beberapa "prosedur". Nilai monadik (tipe ma), adalah prosedur dalam "bahasa pemrograman" ini yang menghasilkan nilai tipe a.
Sebagai contoh:
adalah beberapa prosedur yang mengeluarkan nilai dari tipe Int.
Kemudian:
adalah beberapa prosedur yang menghasilkan dua (mungkin berbeda) Ints.
Setiap "bahasa" tersebut mendukung beberapa operasi:
dua prosedur (ma dan mb) dapat "digabungkan": Anda dapat membuat prosedur yang lebih besar (ma >> mb) yang dibuat dari yang pertama kemudian yang kedua;
terlebih lagi output (a) yang pertama dapat mempengaruhi yang kedua (ma >> = \ a -> ...);
sebuah prosedur (return x) dapat menghasilkan beberapa nilai konstan (x).
Bahasa pemrograman tertanam yang berbeda berbeda pada hal-hal yang mereka dukung seperti:
sumber
Jangan bingung antara tipe monadik dengan kelas monad.
Tipe monadik (yaitu tipe yang merupakan instance dari kelas monad) akan memecahkan masalah tertentu (pada prinsipnya, setiap tipe monadik memecahkan yang berbeda): Negara, Acak, Mungkin, IO. Semuanya adalah tipe dengan konteks (apa yang Anda sebut "label", tetapi bukan itu yang membuat mereka menjadi monad).
Bagi mereka semua, ada kebutuhan "operasi chaining dengan pilihan" (satu operasi tergantung pada hasil sebelumnya). Inilah kelas monad: buat tipe Anda (memecahkan masalah yang diberikan) menjadi instance dari kelas monad dan masalah rantai diselesaikan.
Lihat Apa yang dipecahkan oleh kelas monad?
sumber