Haskell ">>="による副作用管理【Haskell モナド モナド則】

モナドのメリット

モナドHaskellのプログラムにおいて、特に入出力回りで活躍する仕組みです。 関数プログラムの本体から副作用を安全に分離することができ、計算戦略をプログラム全体にばら撒くことをせずに、一箇所にまとめることができます。

モナドの具体例

モナドの具体例1

例えば、二つの関数getLineputStrLn関数を考えます。 この二つはどちらもIO型で定義される、副作用の存在する 関数です。

そして、この二つの関数を用いて、「getLineで得た行を、putStrLnで出力したい」とします。 通常のコードであれば、次のように書き表すと思います。

import System.IO
import Data.Char(toUpper)

main :: IO ()
main = do 
    inpStr <- getLine
    putStrLn( inpStr )

上記のコードでは、getLineで得た内容をinpStr変数に格納しています。 そして次の行でinpStr変数の内容をputStrLnで標準出力しています。

ところが、よくよく考えればinpStrは仲介するだけの変数で、可能であればgetLineputStrLnの関数を直接つなげたくなると思います。

import System.IO
import Data.Char(toUpper)

main :: IO ()
main = do 
    getLine >>= putStrLn

このように、副作用のある関数同士>>= でつなげることができます。

  • getLine関数は 標準入力を受け付ける
  • inpStr関数は 標準出力する
  • >>= で上記の関数をつなげることで、標準入力を標準出力する

具体例その2

上記のコードは、 「標準入力で受け取った内容を、標準出力により表示する」という単純なプログラムでしたが、この課題を次のように発展させましょう。

「標準入力で受け取った内容を、すべて大文字に変えて、標準出力により表示する」というプログラムに置き換えます。

まずは、モナドを使わない例を見てみましょう。

import System.IO
import Data.Char(toUpper)

main :: IO ()
main = do 
    inpStr <- getLine
    let inpStr2 = map toUpper inpStr
    putStrLn( inpStr2 )

上記のコードを実行すると、入力された文字列はすべて大文字となって返却されると思います。

test
TEST

上記のコードでやっていることを、日本語に起こしてみましょう。

  1. getLineで入力した内容を、inpStrへ出力する。
  2. map toUpper inpStrにより得た大文字を、inpStr2へ出力する。
  3. putStrLnにより、inpStr2を表示する。

しかし、上記のコードもまだまだ短くできそうです。

やりたいことは、「標準入力で受け取った内容を、すべて大文字に変えて、標準出力により表示する」という内容だったはずです。 そのために、inpStrinpStr2を用意するのは必要な内容とは思えません。(説明変数の役割は果たせる可能性がありますが、本質的には必要な変数ではないでしょう)

そこで、モナドを使用して、 「標準入力で受け取った内容を、すべて大文字に変えて、標準出力により表示する」 を実現してみましょう。

import System.IO
import Data.Char(toUpper)

main :: IO ()
main = do 
    getLine >>= putStrLn . (\x -> map toUpper x )

上記のコードは、 putStrLn関数に対して「入力された内容をすべて大文字にする関数」を合成 しています。 いわば、「引数をすべて大文字にして表示する」という関数を新しく作ってしまったわけです。

上記のコードは「合成」をうまく使ってやりたいことを一行で表すことが出来ています。

あえて言うのであれば、「入力された内容をすべて大文字にする関数」はまとめることができそうですね。やってみましょう。

import System.IO
import Data.Char(toUpper)

toUppers :: String -> String
toUppers = map toUpper

main :: IO ()
main = do 
    getLine >>= putStrLn . toUppers

3つの関数を華麗に使い、 「標準入力で受け取った内容を、すべて大文字に変えて、標準出力により表示する」 を表現することが出来ました。

モナドの正体?

ここで、モナド関連の演算子によく使われる>>=を詳しく見てみる。 まずは型の詳細をみよう。

Prelude> :t (>>=)
(>>=) :: IO a -> (a -> IO b) -> IO b

この結果をみると、いくつかわかることがある。

  • 第一引数はIO型の変数
      - 第二引数のように、関数としてラッピングされていないことを考えると、外部からの入力結果と考えてよい。
    
  • 第二引数は関数 (a -> IO b)
      - 関数 (a -> IO b) は、型 a の値を受け取り、型 b の値を含むIOアクションを生成します。
    
  • 返り値はIO b
      - 第二引数の関数の返り値のこと。
    

つまり、>>=オペレーターは、外部からの入力がある、副作用の関数Aと、外部へ出力がある副作用のある関数Bを結び付けるという関数であると言えます。

Haskellの (>>=) バインド演算子は、外部の世界とのやり取りを伴うIOアクションを順番に実行する際に使用されます。 具体的には、関数Aが外部からの入力や他の副作用を持つ IO a アクションで、関数Bが外部への出力や他の副作用を持つ (a -> IO b) 関数です。

>>=の型に着目する

具体例1で置き換えて考えてみる。

import System.IO
import Data.Char(toUpper)

main :: IO ()
main = do 
    getLine >>= putStrLn

この場合、getLineIO aの型に当てはまり、 putStrLn(a -> IO b)の型に当てはまる。

せっかくなので、それぞれの型を確認してみる

getLine :: IO String
putStrLn :: String -> IO ()

予想通り、

  • getLineの型はIO付きのString
  • putStrLnの型はString -> IO() であった。

HaskellのRandom関数

Haskellにおけるモナドの理解を進めるために、「ランダムな数値を取得し、標準出力させる」という関数を作りましょう。

まずはモナドの仕組みを使用しないケースを見てみます。

import System.Random

main = do
    randomNumber <- randomRIO (0, 100 :: Int)
    print(randomNumber)

Haskellにおいて乱数を扱うためにはRandomライブラリーを使用します。 今回使用する関数はrandomRIOですが、これは指定した範囲でランダムな数値を返却します。

しかし、Randoom関数においていま最も着目しなければならない点は、これがIO型の数値を返却するという点でしょう。

これに着目し、=<<演算子を使用した場合は、次のように書き直せます。

import System.Random

main = do
    print =<< randomRIO (0, 100 :: Int)

return関数

先ほどはランダムな数値をprint関数によって画面出力しました。

ところで、これまではIO型のみ>>=などのモナド関連の演算子で出力してきましたが、 定数などの外部からの要素でない値を表示することはできないのでしょうか?

結論、returnキーワードを使用することで、>>=をつなげることができます。

次の例は、0をprint関数で画面出力するケースです。

import System.Random

main = do
    (return 0) >>= print