bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

HaskellドリルⅦ IOモナド

1.現実の世界

モナドは二つの演算returnと>>=を有していた。この中の演算(>>=)を連続して用いることにより、同じ型の中で、鎖のように繋がった操作(オペレーション)が可能になった。これを見やすくしたのがdo記法であった。

do記法を用いることで逐次型の処理が可能になる。逐次型の処理で特に重要なものは、入出力を伴う処理であろう。Haskellでは、入出力はIOモンドで行われる。

2.putStrとgetLine

IOモナドの関数の中で、最も基本的な関数はputStrとgetLineである。putStrはディスプレイ(標準入力)に文字列を出力するものであり、getLineは一行の文字列をキーボード(標準入力)から入力するものである。型シグネチャはそれぞれ次のようになっている。

Prelude> :t putStr
putStr :: String -> IO ()
Prelude> :t getLine
getLine :: IO String

さて、一行の文字列を得て、その文字列をそのまま出力するプログラムは次のようになる。

main =
  getLine >>= putStr

これを実行する。関数名mainを打ち込んだ後、「I like Haskell.改行(Enter)」とキーボードから打ち込むと、同じものが返ってくる(ここで、改行は一行の終わりを示すものとして使われ、getLineからの出力は、改行の前までである。この場合はピリオドまでである)。

*Main> main
I like Haskell.
I like Haskell.*Main> 

このプログラムでは出力は改行されない。改行をしたければ、putStrをpurStrLnに変えればよい。即ち、

main =
  getLine >>= putStrLn

とする。もう一度、「I like Haskell.改行(Enter)」とキーボードから打ち込むと、

*Main> main
I like Haskell.
I like Haskell.
*Main> 

となり、改行されていることが分かる。

上のプログラムをdo記法で書き表すと次のようになる。

main = do
  x <- getLine
  putStrLn x

それでは、二つの数を入力してその和を出力するプログラムを考える。二つの数の和はIOモナドではないので、そのまま出力することができないが、これを助けてくれるのが関数printである。printの型シグネチャは次のようになっている。

*Main> :t print
print :: Show a => a -> IO ()

これから、printは型クラスShowのインスタンスaをIOモナドに変換してくれる、ことが分かる。すべての数はShowのインスタンスであるため、数をディスプレイ上に表示したいときは、printを用いればよい。

そこで、プログラムは次のようになる。

main = do
  x <- getLine
  y <- getLine
  print $ read x + read y

上のプログラムで、xとyは文字列として入力されるので、文字列を数に変える必要がある。そこで、それぞれの前にreadがついている。

readの型シグネチャは次のようになっている。

read :: Read a => String -> a

実行結果は、例えば、次のようになる。

*Main> main
45
32
77

3.forMとwhen

モナドが逐次的に実行することから、JavaやCが備えている処理の流れを制御文、Haskellは関数型なのでこの場合は関数になるが、をHaskellでも備えている。それらは、whenとforMという関数である。なお、これらの関数は、Control.monadというモジュールにあるので予めimportしておく必要がある。

例によって型シグネチャを見る。whenは次のようになっている。

*Main> main
*Main> import Control.Monad
*Main Control.Monad> :t when
when :: Monad m => Bool -> m () -> m ()

これからwhenはブールとモナドmを受け取り、モナドmを返すことが分かる。型シグネチャからは分からないが、ブールの値が真のときは、入力したモナドを実行する。そうでないときは、何もしないでreturn()を返すのがwhenの約束である。

whenを用いた例を簡単なプログラムで示す。名前がキーボードから入力され、Johnであれば出力するのだが、それ以外の名前が来たときは出力しないことにする。このプログラムは次のようになる。

import Control.Monad

main = do
  x <- getLine
  when (x == "John") $ do
    putStrLn x

この処理を永遠に続けるためには、foreverという関数を用いて次のようにする。

import Control.Monad

main = forever $ do
  x <- getLine
  when (x == "John") $ do
    putStrLn x

次にforMに移る。forMの型シグネチャは次のようになっている。

forM :: Monad m => [a] -> (a -> m b) -> m [b]

これからわかるように、forMはリストを受け取り、リストの要素を関数でモナドに変換し、そのリストを返すというものである。

forMを用いた簡単なプログラムを例に挙げる。1から10までの数字を入力とし、それぞれの2倍を出力するプログラムは次のようになる。

import Control.Monad

main = do
  forM [1..10] $ \x -> do
    print $ x * 2

これを実行すると次のようになる。

*Main Control.Monad> main
2
4
6
8
10
12
14
16
18
20
[(),(),(),(),(),(),(),(),(),()]

また、HaskellにはモナドのためのmapMもある。これは、forMの引数をフリップしたものと同じである。先のプログラムをmapMで記述すると次のようになる。

mapM (\x -> print $ x * 2) [1..10]

また、m [b]の出力がわずらわしいので、これを出力しないようにするためには、mapMをmapM_で置き換えてればよい。

mapM_ (\x -> print $ x * 2) [1..10]

結果は次のようになる。

*Main Control.Monad> main
2
4
6
8
10
12
14
16
18
20

4.問題

問題1:二つの文字列を入力しそれをつなげるプログラムを(>>=)を用いて作成しなさい。

問題2:上記のプログラムをdo記法で書き表しなさい。

問題3:二つの数を入力しその積を得るプログラムを(>>=)を用いて作成しなさい。

問題4:上記のプログラムをdo記法で書き表しなさい。

問題5:二つのブール値を入力しその論理和を得るプログラムを(>>=)を用いて作成しなさい。

問題6:上記のプログラムをdo記法で書き表しなさい。

問題7:三つの数を入力しその和を得るプログラムを(>>=)を用いて作成しなさい。

問題8:上記のプログラムをdo記法で書き表しなさい。

問題9:whenを用いて、数を連続して入力し、その都度、整数であれば出力し、そうでなければ何もしないプログラムを作成しなさい。

問題10:10001から10010までの数を入力し、それぞれのかすの3倍を出力するプログラムをforMを用いて作成しなさい。また、このプログラムをmapM_で書き直しなさい。