Mengapa efek samping dimodelkan sebagai monad di Haskell?

172

Adakah yang bisa memberikan beberapa petunjuk tentang mengapa perhitungan tidak murni di Haskell dimodelkan sebagai monad?

Maksudku monad hanyalah antarmuka dengan 4 operasi, jadi apa alasan untuk memodelkan efek samping di dalamnya?

bodacydo
sumber
15
Monads hanya mendefinisikan dua operasi.
Dario
3
tetapi bagaimana dengan pengembalian dan kegagalan? (selain (>>) dan (>> =))
bodacydo
55
Dua operasi tersebut adalah returndan (>>=). x >> ysama dengan x >>= \\_ -> y(yaitu mengabaikan hasil argumen pertama). Kami tidak membicarakannya fail.
porges
2
@ Maaf Mengapa tidak berbicara tentang gagal? Ini agak berguna dalam yaitu Maybe, Parser, dll.
alternatif
16
@monadic: failada di Monadkelas karena kecelakaan historis; itu benar-benar milik MonadPlus. Perhatikan bahwa definisi standarnya tidak aman.
JB.

Jawaban:

292

Misalkan fungsi memiliki efek samping. Jika kita mengambil semua efek yang dihasilkannya sebagai parameter input dan output, maka fungsinya murni bagi dunia luar.

Jadi, untuk fungsi yang tidak murni

f' :: Int -> Int

kami menambahkan RealWorld ke dalam pertimbangan

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

kemudian fmurni lagi. Kami mendefinisikan tipe data parametrized type IO a = RealWorld -> (a, RealWorld), jadi kami tidak perlu mengetik RealWorld berkali-kali, dan cukup menulis

f :: Int -> IO Int

Bagi programmer, menangani RealWorld secara langsung terlalu berbahaya — khususnya, jika seorang programmer mendapatkan nilai tipe RealWorld, mereka mungkin mencoba menyalinnya , yang pada dasarnya tidak mungkin. (Bayangkan mencoba menyalin seluruh sistem file, misalnya. Di mana Anda meletakkannya?) Oleh karena itu, definisi IO kami merangkum keadaan seluruh dunia juga.

Komposisi fungsi "tidak murni"

Fungsi-fungsi tidak murni ini tidak berguna jika kita tidak dapat mengikatnya bersama. Mempertimbangkan

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

Kami ingin

  • dapatkan nama file dari konsol,
  • baca file itu, dan
  • cetak konten file itu ke konsol.

Bagaimana kita melakukannya jika kita dapat mengakses keadaan dunia nyata?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Kami melihat polanya di sini. Fungsinya disebut seperti ini:

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Jadi kita dapat mendefinisikan operator ~~~untuk mengikat mereka:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

maka kita cukup menulis

printFile = getLine ~~~ getContents ~~~ putStrLn

tanpa menyentuh dunia nyata.

"Impurifikasi"

Sekarang misalkan kita ingin membuat isi file huruf besar juga. Huruf besar adalah fungsi murni

upperCase :: String -> String

Tetapi untuk membuatnya menjadi dunia nyata, ia harus mengembalikan sebuah IO String. Mudah untuk mengangkat fungsi seperti itu:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

Ini dapat digeneralisasi:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

sehingga impureUpperCase = impurify . upperCase, dan kita bisa menulis

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Catatan: Biasanya kami menulis getLine ~~~ getContents ~~~ (putStrLn . upperCase))

Kami bekerja dengan monads selama ini

Sekarang mari kita lihat apa yang telah kita lakukan:

  1. Kami mendefinisikan operator (~~~) :: IO b -> (b -> IO c) -> IO cyang menghubungkan dua fungsi yang tidak murni secara bersamaan
  2. Kami mendefinisikan fungsi impurify :: a -> IO ayang mengubah nilai murni menjadi tidak murni.

Sekarang kita membuat identifikasi (>>=) = (~~~)dan return = impurify, dan lihat? Kami punya monad.


Catatan teknis

Untuk memastikan itu benar-benar monad, masih ada beberapa aksioma yang perlu diperiksa juga:

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    Dibiarkan sebagai latihan.

kennytm
sumber
5
+1 tetapi saya ingin mencatat bahwa ini secara khusus mencakup kasus IO. blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html sangat mirip, tetapi digeneralisasikan RealWorldmenjadi ... well, Anda akan lihat.
ephemient
4
Perhatikan bahwa penjelasan ini tidak dapat benar-benar berlaku untuk Haskell IO, karena yang terakhir mendukung interaksi, konkurensi, dan nondeterminisme. Lihat jawaban saya untuk pertanyaan ini untuk beberapa petunjuk lebih lanjut.
Conal
2
@Conal GHC benar-benar menerapkan IOcara ini, tetapi RealWorldtidak benar-benar mewakili dunia nyata, itu hanya tanda untuk menjaga operasi agar ("keajaiban" adalah RealWorldsatu-satunya tipe keunikan GHC Haskell)
Jeremy List
2
@JeremyList Seperti yang saya pahami, GHC mengimplementasikan IOmelalui kombinasi representasi ini dan sihir kompiler non-standar (mengingatkan pada virus kompiler C yang terkenal dari Ken Thompson ). Untuk jenis lain, kebenarannya ada dalam kode sumber bersama dengan semantik Haskell yang biasa.
Conal
1
@Clonal Komentar saya adalah karena saya telah membaca bagian yang relevan dari kode sumber GHC.
Daftar Jeremy
43

Adakah yang bisa memberikan beberapa petunjuk tentang mengapa perhitungan tidak murni di Haskell dimodelkan sebagai monad?

Pertanyaan ini mengandung kesalahpahaman yang meluas. Kenajisan dan Monad adalah gagasan independen. Kenajisan tidak dimodelkan oleh Monad. Sebaliknya, ada beberapa tipe data, seperti IO, yang mewakili perhitungan imperatif. Dan untuk beberapa tipe tersebut, sebagian kecil dari antarmuka mereka sesuai dengan pola antarmuka yang disebut "Monad". Selain itu, tidak ada dikenal murni / fungsional / denotatif penjelasan IO(dan ada tidak mungkin satu, mengingat "dosa bin" tujuan IO), meskipun ada cerita umum menceritakan tentang World -> (a, World)menjadi makna IO a. Kisah itu tidak dapat menggambarkan dengan jujur IO, karenaIOmendukung konkurensi dan nondeterminisme. Kisah ini bahkan tidak berfungsi ketika untuk perhitungan deterministik yang memungkinkan interaksi mid-komputasi dengan dunia.

Untuk penjelasan lebih lanjut, lihat jawaban ini .

Sunting : Saat membaca kembali pertanyaan, saya kira jawaban saya tidak sesuai. Model perhitungan imperatif sering berubah menjadi monad, seperti yang dikatakan pertanyaan. Penanya mungkin tidak benar-benar berasumsi bahwa keganjilan dengan cara apa pun memungkinkan pemodelan perhitungan imperatif.

Conal
sumber
1
@ KennyTM: Tetapi RealWorld, seperti yang dikatakan oleh para dokter, "sangat ajaib". Ini token yang mewakili apa yang dilakukan sistem runtime, sebenarnya tidak berarti apa-apa tentang dunia nyata. Anda bahkan tidak dapat menyulap yang baru untuk membuat "utas" tanpa melakukan tipu daya ekstra; pendekatan naif hanya akan membuat tindakan tunggal, memblokir dengan banyak ambiguitas tentang kapan itu akan berjalan.
CA McCann
4
Juga, saya berpendapat bahwa monad pada dasarnya bersifat imperatif. Jika functor mewakili beberapa struktur dengan nilai yang tertanam di dalamnya, instance monad berarti Anda dapat membuat dan meratakan layer baru berdasarkan nilai-nilai itu. Jadi, apa pun makna yang Anda tetapkan untuk satu lapisan functor, sebuah monad berarti Anda dapat membuat sejumlah lapisan tanpa batas dengan gagasan kausalitas yang ketat dari satu ke yang berikutnya. Contoh spesifik mungkin tidak memiliki struktur intrinsik imperatif, tetapi Monadsecara umum memang demikian.
CA McCann
3
Dengan " Monadsecara umum" maksud saya kira-kira forall m. Monad m => ..., yaitu, bekerja pada contoh sewenang-wenang. Hal-hal yang dapat Anda lakukan dengan monad sewenang-wenang adalah hal-hal yang hampir sama persis dengan yang dapat Anda lakukan IO: menerima primitif buram (masing-masing sebagai argumen fungsi, atau dari perpustakaan), membangun no-ops dengan return, atau mengubah nilai dengan cara yang tidak dapat diubah menggunakan (>>=). Inti dari pemrograman dalam monad yang berubah-ubah adalah menghasilkan daftar tindakan yang tidak dapat dibatalkan: "lakukan X, lalu lakukan Y, lalu ...". Kedengarannya sangat penting bagi saya!
CA McCann
2
Tidak, Anda masih kehilangan poin saya di sini. Tentu saja Anda tidak akan menggunakan pola pikir itu untuk jenis-jenis tertentu itu, karena mereka memiliki struktur yang jelas dan bermakna. Ketika saya mengatakan "monad arbitrer", maksud saya "Anda tidak bisa memilih yang mana"; perspektif di sini adalah dari dalam quantifier, jadi berpikir msebagai eksistensial mungkin lebih bermanfaat. Lebih jauh, "interpretasi" saya adalah pengulangan ulang hukum; daftar pernyataan "do X" adalah monoid bebas pada struktur tidak dikenal yang dibuat via (>>=); dan hukum monad hanyalah hukum monoid pada komposisi endofunctor.
CA McCann
3
Singkatnya, batas bawah terbesar yang digambarkan oleh semua monad adalah perjalanan buta dan tak berarti ke masa depan. IOadalah kasus patologis justru karena ia menawarkan hampir tidak lebih dari minimum ini. Dalam kasus tertentu, tipe dapat mengungkapkan lebih banyak struktur, dan dengan demikian memiliki makna aktual; tetapi sebaliknya sifat-sifat esensial dari sebuah monad - berdasarkan pada undang-undang - sama antitesisnya dengan penghapusan denotasi sebagaimana IOadanya. Tanpa mengekspor konstruktor, secara mendalam menyebutkan tindakan primitif, atau sesuatu yang serupa, situasinya menjadi sia-sia.
CA McCann
13

Seperti yang saya pahami, seseorang bernama Eugenio Moggi pertama kali memperhatikan bahwa konstruksi matematika yang sebelumnya tidak jelas yang disebut "monad" dapat digunakan untuk memodelkan efek samping dalam bahasa komputer, dan karenanya menetapkan semantik mereka menggunakan kalkulus Lambda. Ketika Haskell sedang dikembangkan ada berbagai cara di mana perhitungan tidak murni dimodelkan (lihat kertas "kemeja" Simon Peyton Jones untuk lebih jelasnya), tetapi ketika Phil Wadler memperkenalkan monad, dengan cepat menjadi jelas bahwa ini adalah The Answer. Dan sisanya adalah sejarah.

Paul Johnson
sumber
3
Tidak terlalu. Telah diketahui bahwa monad dapat memodelkan interpretasi untuk waktu yang sangat lama (setidaknya sejak "Topoi: Analisis Kategorik Logika). Di sisi lain, itu tidak mungkin untuk secara jelas mengekspresikan jenis-jenis untuk monad sampai sangat fungsional mengetik bahasa muncul, dan kemudian Moggi menyatukan dua dan dua
Nomen
1
Mungkin monad bisa lebih mudah dipahami jika mereka didefinisikan dalam hal bungkus peta dan dibuka dengan kembali menjadi sinonim untuk bungkus.
aoeu256
9

Adakah yang bisa memberikan beberapa petunjuk tentang mengapa perhitungan tidak murni di Haskell dimodelkan sebagai monad?

Ya, karena Haskell murni . Anda memerlukan konsep matematika untuk membedakan antara perhitungan tidak murni dan yang murni pada tipe-tingkat dan untuk memodelkan aliran program di masing-masing.

Ini berarti Anda harus berakhir dengan beberapa jenis IO ayang memodelkan perhitungan yang tidak murni. Maka Anda perlu mengetahui cara-cara menggabungkan perhitungan ini yang berlaku secara berurutan ( >>=) dan mengangkat nilai ( return) adalah yang paling jelas dan mendasar.

Dengan keduanya, Anda telah mendefinisikan sebuah monad (bahkan tanpa memikirkannya);)

Selain itu, monad menyediakan abstraksi yang sangat umum dan kuat , sehingga banyak jenis aliran kontrol dapat dengan mudah digeneralisasikan dalam fungsi-fungsi monadik seperti sequence, liftMatau sintaksis khusus, membuat ketidaksatuan bukan kasus khusus.

Lihat monads dalam pemrograman fungsional dan pengetikan unik (satu-satunya alternatif yang saya tahu) untuk informasi lebih lanjut.

Dario
sumber
6

Seperti yang Anda katakan, Monadadalah struktur yang sangat sederhana. Separuh dari jawabannya adalah: Monadadalah struktur paling sederhana yang dapat kita berikan untuk fungsi-fungsi efek samping dan dapat menggunakannya. Dengan Monadkita dapat melakukan dua hal: kita dapat memperlakukan nilai murni sebagai nilai efek samping (return ), dan kita dapat menerapkan fungsi efek samping ke nilai efek samping untuk mendapatkan nilai efek samping baru ( >>=). Kehilangan kemampuan untuk melakukan salah satu dari hal-hal ini akan melumpuhkan, jadi tipe efek samping kami harus "setidaknya" Monad, dan ternyata Monadcukup untuk mengimplementasikan semua yang kami butuhkan sejauh ini.

Setengah lainnya adalah: apa struktur paling detail yang bisa kita berikan untuk "kemungkinan efek samping"? Kita tentu bisa memikirkan ruang semua efek samping yang mungkin sebagai satu set (satu-satunya operasi yang membutuhkan adalah keanggotaan). Kita dapat menggabungkan dua efek samping dengan melakukannya satu per satu, dan ini akan menimbulkan efek samping yang berbeda (atau mungkin efek yang sama - jika yang pertama adalah "mematikan komputer" dan yang kedua adalah "menulis file", maka hasilnya menyusun ini hanya "mematikan komputer").

Ok, jadi apa yang bisa kita katakan tentang operasi ini? Itu asosiatif; yaitu, jika kita menggabungkan tiga efek samping, tidak masalah urutan mana yang kita lakukan menggabungkan. Jika kita melakukan (menulis file kemudian membaca soket) kemudian mematikan komputer, itu sama seperti melakukan menulis file kemudian (baca soket lalu shutdown komputer). Tapi itu tidak komutatif: ("tulis file" lalu "hapus file") adalah efek samping yang berbeda dari ("hapus file" lalu "tulis file"). Dan kami memiliki identitas: efek samping khusus "tidak ada efek samping" berfungsi ("tidak ada efek samping" lalu "hapus file" adalah efek samping yang sama dengan hanya "hapus file") Pada titik ini setiap matematikawan berpikir "Grup!" Tetapi kelompok memiliki invers, dan tidak ada cara untuk membalikkan efek samping secara umum; "menghapus berkas" tidak dapat dipulihkan. Jadi struktur yang tersisa adalah monoid, yang berarti fungsi efek sampingnya adalah monad.

Apakah ada struktur yang lebih kompleks? Tentu! Kita dapat membagi efek samping yang mungkin menjadi efek berbasis sistem file, efek berbasis jaringan dan banyak lagi, dan kita bisa membuat aturan komposisi yang lebih rumit yang menjaga detail ini. Tapi sekali lagi turun ke: Monadsangat sederhana, namun cukup kuat untuk mengekspresikan sebagian besar properti yang kita pedulikan. (Khususnya, asosiatif dan aksioma lainnya mari kita uji aplikasi kita dalam potongan kecil, dengan keyakinan bahwa efek samping dari aplikasi gabungan akan sama dengan kombinasi efek samping dari potongan-potongan).

lmm
sumber
4

Ini sebenarnya cara yang cukup bersih untuk memikirkan I / O secara fungsional.

Di sebagian besar bahasa pemrograman, Anda melakukan operasi input / output. Di Haskell, bayangkan menulis kode bukan untuk melakukan operasi, tetapi untuk menghasilkan daftar operasi yang ingin Anda lakukan.

Monads hanya sintaks yang cukup untuk itu.

Jika Anda ingin tahu mengapa monad bukan yang lain, saya kira jawabannya adalah mereka adalah cara fungsional terbaik untuk mewakili I / O yang bisa dipikirkan orang ketika mereka membuat Haskell.

Noah Lavine
sumber
3

AFAIK, alasannya adalah untuk dapat memasukkan pemeriksaan efek samping dalam sistem tipe. Jika Anda ingin tahu lebih banyak, dengarkan episode - episode SE-Radio : Episode 108: Simon Peyton Jones tentang Pemrograman Fungsional dan Haskell Episode 72: Erik Meijer di LINQ

Gabriel Ščerbák
sumber
2

Di atas ada jawaban terinci yang sangat bagus dengan latar belakang teoretis. Tapi saya ingin memberikan pandangan saya tentang IO monad. Saya tidak berpengalaman dengan programmer haskell, jadi mungkin itu sangat naif atau bahkan salah. Tetapi saya membantu saya untuk berurusan dengan IO monad sampai batas tertentu (perhatikan, bahwa itu tidak berhubungan dengan monad lain).

Pertama saya ingin mengatakan, bahwa contoh dengan "dunia nyata" tidak terlalu jelas bagi saya karena kita tidak dapat mengakses (dunia nyata) sebelumnya. Mungkin itu tidak berhubungan dengan perhitungan monad sama sekali tetapi diinginkan dalam arti transparansi referensial, yang umumnya disajikan dalam kode haskell.

Jadi kami ingin bahasa kami (haskell) murni. Tetapi kami membutuhkan operasi input / output karena tanpa mereka program kami tidak dapat berguna. Dan operasi itu tidak bisa murni berdasarkan sifatnya. Jadi satu-satunya cara untuk menangani ini kita harus memisahkan operasi yang tidak murni dari sisa kode.

Di sini monad datang. Sebenarnya, saya tidak yakin, bahwa tidak ada konstruksi lain dengan properti yang diperlukan serupa, tetapi intinya adalah bahwa monad memiliki properti ini, sehingga dapat digunakan (dan berhasil digunakan). Properti utama adalah bahwa kita tidak dapat melarikan diri darinya. Antarmuka Monad tidak memiliki operasi untuk menyingkirkan monad di sekitar nilai kami. Monad lain (bukan IO) menyediakan operasi seperti itu dan memungkinkan pencocokan pola (mis. Mungkin), tetapi operasi itu tidak berada dalam antarmuka monad. Properti lain yang diperlukan adalah kemampuan untuk rantai operasi.

Jika kita berpikir tentang apa yang kita butuhkan dalam hal sistem tipe, kita sampai pada fakta bahwa kita perlu mengetik dengan konstruktor, yang dapat dililitkan di setiap lembah. Konstruktor harus bersifat pribadi, karena kami melarang melarikan diri darinya (yaitu pencocokan pola). Tetapi kita perlu fungsi untuk memberi nilai pada konstruktor ini (di sini kembali ke pikiran). Dan kita perlu cara untuk rantai operasi. Jika kita memikirkannya selama beberapa waktu, kita akan sampai pada fakta, bahwa operasi rangkaian harus memiliki tipe seperti >> = miliki. Jadi, kita sampai pada sesuatu yang sangat mirip dengan Monad. Saya pikir, jika kita sekarang menganalisis kemungkinan situasi yang bertentangan dengan konstruksi ini, kita akan sampai pada aksioma monad.

Perhatikan, konstruk yang dikembangkan tidak memiliki kesamaan dengan kenajisan. Itu hanya memiliki properti, yang kami harapkan harus mampu menangani operasi yang tidak murni, yaitu, tidak melarikan diri, merantai, dan cara untuk masuk.

Sekarang beberapa rangkaian operasi yang tidak murni ditentukan oleh bahasa dalam monad IO yang dipilih ini. Kami dapat menggabungkan operasi-operasi itu untuk membuat operasi tidak murni yang baru. Dan semua operasi itu harus memiliki IO dalam tipenya. Perhatikan bahwa kehadiran IO dalam beberapa fungsi tidak membuat fungsi ini tidak murni. Tapi seperti yang saya mengerti, itu adalah ide yang buruk untuk menulis fungsi murni dengan IO di tipe mereka, karena pada awalnya ide kami untuk memisahkan fungsi murni dan tidak murni.

Akhirnya, saya ingin mengatakan, bahwa monad tidak mengubah operasi yang tidak murni menjadi murni. Ini hanya memungkinkan untuk memisahkan mereka secara efektif. (Saya ulangi, bahwa itu hanya pemahaman saya)

Dmitrii Semikin
sumber
1
Mereka membantu Anda mengetik memeriksa program Anda dengan membiarkan Anda mengetikkan efek pemeriksaan, dan Anda dapat menentukan DSL Anda sendiri dengan membuat monad untuk membatasi efek yang dapat dilakukan fungsi Anda sehingga kompiler dapat memeriksa kesalahan pengurutan Anda.
aoeu256
Komentar dari aoeu256 ini adalah "mengapa" yang tidak ada dalam semua penjelasan yang diberikan sejauh ini. (yaitu: monad bukan untuk manusia, tetapi untuk penyusun)
João Otero