Tampilan # 1 dan # 2 secara umum salah.
- Jenis data apa pun
* -> *
dapat berfungsi sebagai label, monad jauh lebih dari itu.
- (Dengan pengecualian
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 nilai x :: 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 miliki f :: a -> b
dan g :: b -> c
kita dapatkan g . f :: a -> c
. Perhatikan bahwa ini juga berfungsi untuk nilai yang dikonversi: Jika kami memiliki x :: a
dan mengonversinya menjadi representasi kami, kami mendapatkannya f . ((\_ -> x) :: () -> a) :: () -> b
.
Representasi ini memiliki beberapa sifat yang sangat penting, yaitu:
- Kami memiliki fungsi yang sangat istimewa - fungsi identitas
id :: a -> a
untuk setiap jenis a
. Ini adalah elemen identitas sehubungan dengan .
: f
sama dengan f . id
dan untuk id . f
.
- Operator komposisi fungsi
.
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 tipe m :: * -> *
. (Dalam Haskell, m
adalah []
, IO
, Maybe
, dll) dan kategori akan berisi semua fungsi dari jenis a -> 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:
- Untuk memiliki operator (sebut saja
<=<
) yang menyusun fungsi f :: a -> m b
dan g :: b -> m c
menjadi sesuatu sebagai g <=< f :: a -> m c
. Dan, itu harus asosiatif.
- Untuk memiliki beberapa fungsi identitas untuk setiap jenis, sebut saja
return
. Kami juga ingin itu f <=< return
sama f
dan sama dengan return <=< f
.
Apa pun m :: * -> *
yang kami punya fungsi seperti itu return
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 oleh m
.
(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 tipe m a
dan kami menyusun fungsi tipe a -> 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 menggunakan return
dan >>=
mereka disebut hukum monad .
Contoh - daftar
Jika kita memilih m
untuk menjadi []
, kita mendapatkan kategori fungsi tipe a -> [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 . Komposisi f :: a -> [b]
dan g :: b -> [c]
berfungsi sebagai berikut: g <=< f :: a -> [c]
berarti menghitung semua hasil yang mungkin dari jenis [b]
, berlaku g
untuk masing-masing, dan mengumpulkan semua hasil dalam satu daftar. Disajikan dalam Haskell
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
atau menggunakan >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
Perhatikan bahwa dalam contoh ini, tipe-tipe kembalinya [a]
sangat mungkin sehingga mereka tidak mengandung nilai tipe apa pun a
. Memang, tidak ada persyaratan untuk monad sehingga tipe pengembalian harus memiliki nilai seperti itu. Beberapa monad selalu memiliki (suka IO
atau State
), tetapi beberapa tidak, suka []
atau Maybe
.
IO Monad
Seperti yang saya sebutkan, IO
monad agak istimewa. Nilai tipe IO a
berarti nilai tipe yang a
dibangun dengan berinteraksi dengan lingkungan program. Jadi (tidak seperti semua monad lainnya), kita tidak dapat menggambarkan nilai tipe IO a
menggunakan beberapa konstruksi murni. Berikut IO
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:
- Komposisi
f :: a -> IO b
dan g :: b -> IO c
cara: Menghitung f
yang berinteraksi dengan lingkungan, dan kemudian menghitung g
yang menggunakan nilai dan menghitung hasil yang berinteraksi dengan lingkungan.
return
cukup tambahkan IO
"tag" ke nilai (kami hanya "menghitung" hasilnya dengan menjaga lingkungan tetap utuh).
- Hukum monad (asosiatif, identitas) dijamin oleh kompiler.
Beberapa catatan:
- Karena perhitungan monadik selalu memiliki tipe hasil
m a
, tidak ada cara bagaimana "melarikan diri" dari IO
monad. Artinya adalah: Setelah perhitungan berinteraksi dengan lingkungan, Anda tidak dapat membuat perhitungan dari itu yang tidak.
- Ketika seorang programmer fungsional tidak tahu bagaimana membuat sesuatu dengan cara murni, (s) ia dapat (sebagai upaya terakhir ) memprogram tugas dengan beberapa perhitungan stateful dalam
IO
monad. Inilah sebabnya mengapa IO
sering disebut sin bin programmer .
- Perhatikan bahwa dalam dunia yang tidak murni (dalam arti pemrograman fungsional) membaca nilai dapat mengubah lingkungan juga (seperti mengonsumsi input pengguna). Itu sebabnya fungsi seperti
getChar
harus memiliki tipe hasil IO something
.