Haskell では副作用がモナドとしてモデル化されるのはなぜですか? 質問する

Haskell では副作用がモナドとしてモデル化されるのはなぜですか? 質問する

Haskell の不純な計算がモナドとしてモデル化される理由について、どなたかアドバイスをいただけませんか?

つまり、モナドは 4 つの操作を持つインターフェースにすぎないのに、その中で副作用をモデル化する理由は何だったのでしょうか?

ベストアンサー1

関数に副作用があるとします。その関数が生成するすべての効果を入力パラメータと出力パラメータとして受け取ると、その関数は外部に対して純粋になります。

つまり、不純な関数の場合

f' :: Int -> Int

現実世界も考慮に入れる

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.

f再び純粋になります。パラメータ化されたデータ型を定義するtype IO a = RealWorld -> (a, RealWorld)と、RealWorldを何度も入力する必要がなくなり、次のように書くことができます。

f :: Int -> IO Int

プログラマーにとって、RealWorldを直接扱うのはあまりにも危険です。特に、プログラマーがRealWorld型の値を手にした場合、コピーそれは基本的に不可能です。(たとえば、ファイルシステム全体をコピーしようと考えてみましょう。どこに保存しますか?) したがって、IO の定義は、世界全体の状態もカプセル化します。

「不純な」関数の合成

これらの不純な関数は、連結できなければ役に立たない。

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

私たちは

  • 得るコンソールからのファイル名、
  • 読むそのファイル、そして
  • 印刷そのファイルの内容をコンソールに表示します。

現実世界の状態にアクセスできるとしたら、どうすればいいでしょうか?

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

ここでパターンがわかります。関数は次のように呼び出されます。

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

~~~そこで、それらをバインドする演算子を定義できます。

(~~~) :: (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

そうすれば、単純に

printFile = getLine ~~~ getContents ~~~ putStrLn

現実世界に触れることなく。

「不浄化」

ここで、ファイルの内容を大文字にしたいとします。大文字化は純粋な関数です。

upperCase :: String -> String

しかし、これを現実世界に持ち込むには、 を返す必要がありますIO String。このような関数を持ち上げる方法は簡単です。

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

これを一般化すると次のようになります。

impurify :: a -> IO a

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

となるのでimpureUpperCase = impurify . upperCase、次のように書くことができる。

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

(注:通常は と書きますgetLine ~~~ getContents ~~~ (putStrLn . upperCase)

私たちはずっとモナドを扱っていた

では、何をしたか見てみましょう:

  1. (~~~) :: IO b -> (b -> IO c) -> IO c2つの不純な関数を連結する演算子を定義した。
  2. impurify :: a -> IO a純粋な値を不純な値に変換する関数を定義しました。

(>>=) = (~~~)ここで、と を識別してみますreturn = impurify。わかりますか? モナドが得られます。


技術ノート

それが本当にモナドであることを確認するには、まだチェックする必要がある公理がいくつかあります。

  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

    練習として残しました。

おすすめ記事