Berbagai cara untuk melihat monad

29

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?

Oni
sumber
5
# 2 bukan seperti apa Monad pada umumnya. Faktanya, ini cukup terbatas untuk IO, dan bukan pandangan yang berguna (lih. Apa yang bukan Monad ). Juga, "ketat" umumnya diambil untuk menamai properti yang Haskell tidak miliki (yaitu evaluasi ketat). Ngomong-ngomong, Monads juga tidak mengubahnya (sekali lagi, lihat Apa yang bukan Monad).
3
Secara teknis, hanya yang ketiga yang benar. Monad adalah endofunctor, yang didefinisikan sebagai operasi khusus - promosi dan mengikat. Monad banyak - daftar monad adalah contoh sempurna untuk mendapatkan intuisi di belakang monad. Fasilitas baca bahkan lebih baik. Cukup mengejutkan, monad dapat digunakan sebagai alat untuk menyisipkan status dalam bahasa fungsional murni secara implisit. Ini bukan properti pendefinisian monad: ini adalah kebetulan, bahwa threading negara dapat diimplementasikan dalam istilah mereka. Hal yang sama berlaku untuk IO.
permeakra
Common Lisp memiliki kompiler sendiri sebagai bagian dari bahasa. Haskell memiliki Monads.
Will Ness

Jawaban:

33

Tampilan # 1 dan # 2 secara umum salah.

  1. Jenis data apa pun * -> *dapat berfungsi sebagai label, monad jauh lebih dari itu.
  2. (Dengan pengecualian IOmonad) 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 IOmonad, 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 -> bdan g :: b -> ckita dapatkan g . f :: a -> c. Perhatikan bahwa ini juga berfungsi untuk nilai yang dikonversi: Jika kami memiliki x :: adan 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 identitasid :: a -> a untuk setiap jenis a. Ini adalah elemen identitas sehubungan dengan .: fsama dengan f . iddan 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 mjenis * -> *(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, madalah [], 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 bdan g :: b -> m cmenjadi 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 <=< returnsama fdan sama dengan return <=< f.

Apa pun m :: * -> *yang kami punya fungsi seperti itu returndan <=<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 adan kami menyusun fungsi tipe a -> m b.

Untuk setiap monad yang kita buat, kita tidak boleh lupa untuk memeriksanya returndan <=<memiliki properti yang kami butuhkan: asosiatif dan identitas kiri / kanan. Dinyatakan menggunakan returndan >>=mereka disebut hukum monad .

Contoh - daftar

Jika kita memilih muntuk 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 guntuk 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 IOatau State), tetapi beberapa tidak, suka []atau Maybe.

IO Monad

Seperti yang saya sebutkan, IOmonad agak istimewa. Nilai tipe IO aberarti nilai tipe yang adibangun dengan berinteraksi dengan lingkungan program. Jadi (tidak seperti semua monad lainnya), kita tidak dapat menggambarkan nilai tipe IO amenggunakan beberapa konstruksi murni. Berikut IOini hanyalah tag atau label yang membedakan komputasi yang berinteraksi dengan lingkungan. Ini (satu-satunya kasus) di mana tampilan # 1 dan # 2 benar.

Untuk IOmonad:

  • Komposisi f :: a -> IO bdan g :: b -> IO ccara: Menghitung fyang berinteraksi dengan lingkungan, dan kemudian menghitung gyang menggunakan nilai dan menghitung hasil yang berinteraksi dengan lingkungan.
  • returncukup tambahkan IO"tag" ke nilai (kami hanya "menghitung" hasilnya dengan menjaga lingkungan tetap utuh).
  • Hukum monad (asosiatif, identitas) dijamin oleh kompiler.

Beberapa catatan:

  1. Karena perhitungan monadik selalu memiliki tipe hasil m a, tidak ada cara bagaimana "melarikan diri" dari IOmonad. Artinya adalah: Setelah perhitungan berinteraksi dengan lingkungan, Anda tidak dapat membuat perhitungan dari itu yang tidak.
  2. 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 IOmonad. Inilah sebabnya mengapa IOsering disebut sin bin programmer .
  3. 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 getCharharus memiliki tipe hasil IO something.
Petr Pudlák
sumber
3
Jawaban yang bagus Saya akan mengklarifikasi bahwa IOtidak 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 menganggap IOsebagai elemen bahasa khusus atau tag deklaratif. Bukan itu.
usr
2
@ usr Poin bagus. Saya akan menambahkan bahwa unsafePerformIO benar-benar tidak aman dan hanya boleh digunakan oleh para ahli. Hal ini memungkinkan Anda untuk menghancurkan segalanya, misalnya, Anda dapat membuat fungsi coerce :: a -> byang mengubah dua jenis (dan crash program Anda dalam banyak kasus). Lihat contoh ini - Anda dapat mengubah bahkan suatu fungsi menjadi Intdll.
Petr Pudlák
Monad "sihir khusus" lainnya adalah ST, yang memungkinkan Anda mendeklarasikan referensi ke memori yang dapat Anda baca dan tulis sesuai keinginan (meskipun hanya di dalam monad), dan kemudian Anda dapat mengekstraksi hasilnya dengan memanggilrunST :: (forall s. GHC.ST.ST s a) -> a
sara
5

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:

foo :: IO Int

adalah beberapa prosedur yang mengeluarkan nilai dari tipe Int.

Kemudian:

bar :: IO (Int, Int)
bar = do
  a <- foo
  b <- foo
  return (a,b)

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:

  • menghasilkan nilai acak;
  • "forking" (the [] monad);
  • pengecualian (lempar / tangkap) (The Either e monad);
  • dukungan kelanjutan / callcc eksplisit;
  • mengirim / menerima pesan ke "agen" lain;
  • membuat, mengatur, dan membaca variabel (lokal ke bahasa pemrograman ini) (monad ST).
ysdx
sumber
1

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?

warga negara1
sumber