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 f
murni 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:
- Kami mendefinisikan operator
(~~~) :: IO b -> (b -> IO c) -> IO c
yang menghubungkan dua fungsi yang tidak murni secara bersamaan
- Kami mendefinisikan fungsi
impurify :: a -> IO a
yang 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:
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
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
f >>= (\x -> g x >>= h) = (f >>= g) >>= h
Dibiarkan sebagai latihan.