1. ホーム
  2. haskell

[解決済み] Haskellで副作用がモナドとしてモデル化されているのはなぜですか?

2022-04-20 02:14:38

質問

Haskellの不純物計算がなぜモナドとしてモデル化されているのか、どなたかご指摘ください。

モナドは4つの演算を持つ単なるインタフェースですが、そこに副作用をモデル化する理由は何だったのでしょうか?

解決方法は?

ある関数に副作用があるとします。その関数が生み出す効果をすべて入出力パラメータとするならば、その関数は外界に対して純粋である。

では、不純物の多い関数の場合

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 c これは、2つの不純物関数を連結する
  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

    エクササイズとして残す。