Mengapa kita membutuhkan monad?

366

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?

warga negara1
sumber
4
Penelitian apa yang sudah Anda lakukan? Di mana Anda melihat? Sumber apa yang Anda temukan? Kami berharap Anda melakukan sejumlah besar penelitian sebelum bertanya, dan tunjukkan pada kami pertanyaan tentang penelitian apa yang telah Anda lakukan . Ada banyak sumber daya yang mencoba menjelaskan motivasi untuk sumber daya - jika Anda belum menemukan sama sekali, Anda mungkin perlu melakukan sedikit riset lebih lanjut. Jika Anda menemukan beberapa tetapi mereka tidak membantu Anda, itu akan membuat ini menjadi pertanyaan yang lebih baik jika Anda menjelaskan apa yang Anda temukan dan mengapa mereka tidak bekerja untuk Anda.
DW
8
Ini jelas lebih cocok untuk Programmer.StackExchange dan tidak cocok untuk StackOverflow. Saya akan memilih untuk bermigrasi jika saya bisa, tetapi saya tidak bisa. = (
jpmc26
3
@ jpmc26 Kemungkinan besar akan ditutup di sana sebagai "terutama berdasarkan pendapat"; di sini setidaknya ada peluang (seperti yang ditunjukkan oleh banyaknya upvotes, buka kembali cepat kemarin, dan belum ada lagi suara yang dekat)
Izkata

Jawaban:

580

Mengapa kita membutuhkan monad?

  1. Kami ingin memprogram hanya menggunakan fungsi . ("pemrograman fungsional (FP)" setelah semua).
  2. 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 gdan kemudian f, cukup tulis f(g(x,y)). Dengan cara ini, "program" adalah fungsi juga: main = f(g(x,y)). OKE, tapi ...

  3. 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 izinkan g : Real,Real -> Real | Nothing(fungsi dari dua real menjadi (nyata atau tidak sama sekali)).

  4. 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 ...

  5. Apa yang terjadi sekarang f(g(x,y))? fbelum siap untuk mengkonsumsi Maybe Real. Dan, kami tidak ingin mengubah setiap fungsi yang kami dapat hubungkan dengan guntuk mengkonsumsi a Maybe 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 / tulis gke f). Kami ingin >>=mendapatkan ghasil, memeriksanya dan, kalau-kalau Nothingtidak menelepon fdan kembali Nothing; atau sebaliknya, ekstrak kotak Realdan beri makan fdengan itu. (Algoritma ini hanya implementasi >>=untuk Maybetipe). Perhatikan juga bahwa >>=harus ditulis hanya sekali per "tipe tinju" (kotak yang berbeda, algoritma adaptasi yang berbeda).

  6. 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 gitu mengembalikan "nilai kotak" tersebut. 2. Memiliki komposer / penghubung g >>= funtuk membantu menghubungkan gkeluaran ke finput, jadi kami tidak perlu mengubahnya fsama sekali.

  7. 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: IOmonad.

Kebahagiaan total!

cibercitizen1
sumber
64
@Carl Harap tulis jawaban yang lebih baik untuk mencerahkan kami
XrXr
15
@Carl Saya pikir jelas dalam jawaban bahwa ada banyak masalah yang mendapat manfaat dari pola ini (poin 6) dan bahwa IOmonad hanyalah satu lagi masalah dalam daftar IO(poin 7). Di sisi lain IOhanya muncul sekali dan pada akhirnya, jadi, jangan mengerti "sebagian besar waktu Anda berbicara ... tentang IO".
cibercitizen1
4
Kesalahpahaman besar tentang monad: monad tentang negara; monad tentang penanganan eksepsi; tidak ada cara untuk menerapkan IO di FPL murni tanpa monad, monad tidak ambigu (contrargument is Either). Jawaban terbanyak adalah tentang "Mengapa kita membutuhkan functors?".
vlastachu
4
"6. 2. Miliki seorang komposer / penghubung g >>= funtuk membantu menghubungkan goutput ke finput, jadi kami tidak perlu mengubahnya fsama sekali." ini sama sekali tidak benar . Sebelumnya, dalam f(g(x,y)), fbisa menghasilkan apa pun. Itu bisa saja f:: Real -> String. Dengan "komposisi monadik" itu harus diubah untuk menghasilkan Maybe String, atau jenis tidak cocok. Apalagi, >>=itu sendiri tidak cocok !! Itu >=>yang melakukan komposisi ini, bukan >>=. Lihat diskusi dengan dfeuer di bawah jawaban Carl.
Will Ness
3
Jawaban Anda benar dalam arti bahwa monad IMO memang paling tepat digambarkan sebagai tentang komposisi / fungsi "fungsi" (panah Kleisli benar-benar), tetapi rincian yang tepat dari jenis apa yang terjadi di mana adalah apa yang membuat mereka "monad". Anda bisa mengirimkan kotak-kotak itu dalam segala jenis perilaku (seperti Functor, dll.). Ini cara khusus menghubungkan mereka bersama adalah apa yang mendefinisikan "monad".
Will Ness
219

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 IOjenis menangani itu baik-baik saja dengan sendirinya. Yang ada desugaring monadik dari doblok bisa diganti dengan desugaring untuk bindIO, returnIOdan failIOsebagaimana didefinisikan dalam GHC.Basemodul. (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)dan bar :: (a -> Maybe b)? foo . bartidak mengetik centang, karena bdan Maybe bbukan tipe yang sama.

Tapi ... itu hampir benar. Anda hanya ingin sedikit kelonggaran. Anda ingin dapat memperlakukan Maybe bseolah-olah pada dasarnya b. 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 (.)dan idmembentuk konstruk matematika dikenal sebagai kategori . Tetapi ada cara lain untuk membentuk kategori. Kategori Kleisli, misalnya, memungkinkan objek yang dikomposisi sedikit diperbesar. Kategori Kleisli untuk Maybeterdiri dari (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)dan id :: a -> Maybe a. Artinya, objek dalam kategori menambah (->)dengan Maybe, 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 jenis Maybe. Mereka bekerja dengan setiap jenis yang dapat mengumpulkan kategori yang tepat, mematuhi hukum kategori.

  1. Identitas kiri: id . f=f
  2. Identitas yang benar: f . id=f
  3. Asosiatif: 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's returnadalah sama dengan Kleisli id. 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 Monadabstraksi 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.

Carl
sumber
2
Bisakah Anda menjelaskan hubungan antara kategori secara umum dan kategori Kleisli? Tiga undang-undang yang Anda uraikan berlaku dalam kategori apa pun.
dfeuer
1
@ PDFer Oh Untuk memasukkannya ke dalam kode newtype Kleisli m a b = Kleisli (a -> m b),. Kategori Kleisli adalah fungsi di mana tipe pengembalian kategori ( bdalam hal ini) adalah argumen untuk konstruktor tipe m. Iff Kleisli mmembentuk kategori, madalah Monad.
Carl
1
Apa yang dimaksud dengan tipe pengembalian kategoris? Kleisli mtampaknya membentuk kategori yang objeknya adalah tipe Haskell dan sedemikian rupa sehingga panah dari ake badalah fungsi dari ake m b, dengan id = returndan (.) = (<=<). Apakah itu benar, atau apakah saya mencampur berbagai tingkat hal atau sesuatu?
dfeuer
1
@ PDFeuer Benar. Objek adalah semua tipe, dan morfismanya ada di antara tipe adan b, tetapi mereka bukan fungsi sederhana. Mereka dihiasi dengan tambahan mnilai pengembalian fungsi.
Carl
1
Apakah terminologi Teori Kategori benar-benar diperlukan? Mungkin, Haskell akan lebih mudah jika Anda mengubah jenis menjadi gambar di mana jenisnya akan menjadi DNA untuk bagaimana gambar diambil (tipe tergantung *), dan kemudian Anda menggunakan gambar untuk menulis program Anda dengan nama menjadi karakter ruby ​​kecil di atas ikon.
aoeu256
24

Benjamin Pierce berkata dalam TAPL

Suatu sistem tipe dapat dianggap sebagai penghitungan semacam perkiraan statis terhadap perilaku run-time dari istilah dalam suatu program.

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 filterfungsi filter (> 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, adalah

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]

Untuk mendapatkan semuanya i, seperti itu i <= 10, sum [1..i] > 4, sum [1..i] < 25, kita bisa menulis

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

yang sama dengan [3,4,5,6].

Atau kita dapat mendefinisikan kembali nubfungsi, yang menghapus elemen duplikat dari daftar, dalam hal filterAccum:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

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 ( notElemsebenarnya 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 dari Control.Monadmodul:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterMmengeksekusi aksi monadik untuk semua elemen dari daftar, menghasilkan elemen, untuk mana tindakan monadik kembali True.

Contoh pemfilteran dengan larik:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

mencetak [1,2,4,5,3,8,9]seperti yang diharapkan.

Dan versi dengan IO monad, yang menanyakan elemen apa yang harus dikembalikan:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

Misalnya

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

Dan sebagai ilustrasi terakhir, filterAccumdapat didefinisikan dalam hal filterM:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

dengan StateTmonad, 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.

pengguna3237465
sumber
1
Jawaban ini menjelaskan, mengapa kita membutuhkan typeclass Monad. Cara terbaik untuk memahami, mengapa kita membutuhkan monad dan bukan sesuatu yang lain, adalah membaca tentang perbedaan antara monad dan fungsi aplikatif: satu , dua .
user3237465
20

Saya tidak berpikir IOharus 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:

main :: String -> String
main _ = "Hello World"

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?

data Output = TxtOutput String
            | Beep Frequency

main :: String -> [Output]
main _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

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 ...

readFile :: Filepath -> (String -> [Output]) -> [Output]

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.

data IO = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main :: String -> [IO₀]
main _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

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 hanya Output. Itu pasti termasuk output teks sederhana, tetapi juga memungkinkan membaca file tambahan dll.

data IO = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main :: String -> [IO₁]
main _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

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.

data IO = TxtOut String IO
         | TxtIn (String -> IO₂)
         | Terminate

main :: IO
main = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

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.

getTime :: (UTCTime -> IO₂) -> IO
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO

Jelas ada pola di sini, dan sebaiknya kita menuliskannya sebagai

type IO a = (a -> IO₂) -> IO    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO UTCTime
randomRIO :: Random r => (r,r) -> IO r
findFile :: RegEx -> IO (Maybe FilePath)

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:

data IO a = TxtOut String (IO a)
           | TxtIn (String -> IO a)
           | TerminateWith a

txtOut :: String -> IO ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO String
txtIn = TxtIn $ TerminateWith

instance Functor IO where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

Jelas ini bukan implementasi IO yang efisien, tetapi pada prinsipnya dapat digunakan.

leftaroundabout
sumber
@jdlugosz: 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.
leftaroundtentang
4

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, binddan returnharus memenuhi dua persamaan (identitas kiri dan kanan), juga disebut hukum monad. (Atau seseorang dapat mendefinisikan monads untuk memiliki flattening operationalih - 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:

perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

Berikut adalah contoh repl sesi:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

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.

heisenbug
sumber
3

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"

mljrg
sumber
2

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, Stringdan Realfungsi tipe Int -> String, String -> Realdan sebagainya. Anda dapat menggabungkan fungsi-fungsi ini dengan mudah, diakhiri dengan Int -> 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 Maybeadalah konstruktor tipe. Dibutuhkan jenis, suka Intdan mengembalikan jenis baru Maybe 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 Stringdan String -> 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 > == .

jdinunzio
sumber
2
Ini tidak ada hubungannya dengan keluarga tipe. Apa yang sebenarnya kamu bicarakan?
dfeuer