tak0kadaの何でもノート

発声練習、生存確認用。

医学関連は 医学ノート

「Learn You a Haskell for Great Good!」第8章を読んだ

第8章はIOが何か、IOでできること、IOの実行タイミングについて。

副作用の分離

副作用(結局副作用は何なのか)を起こしてもいいのがIOだそうです。以上。

Hello World!

helloworld.hsを下のように作って、

main = putStrLn "hello, world"
runhaskell helloworld.hs

−− あるいは
ghc -make helloworld
./helloworld

すればhello, world!と表示される。関数mainを実行する際にIOは実行される。

ここで型を見てみると、

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

IO ()である。関数には通常返り値が存在するが、IOを実行した関数からの返り値は無いのでダミーとして空のタプルが使われている。(つまりIO sometypeがmainの型。)

普通は書かないが、mainに型を与えてやって、

main :: IO()
main = ...

としても同じ結果になる。

IOをまとめる

do構文やIOアクションから値を取り出すには<-を使う。

main = do
    putStrLn "Hello, what's your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name ++ "!")
> :t getLine
getLine :: IO String

なのでgetLineはIOアクションを起こして、その結果の値をString型で返すことが分かる。この振る舞いはgetLineだけでなくIOをもつ関数一般にも言える。

そこで、このファイルは次のようにも書ける。

main = do
    foo <- putStrLn "Hello, what's your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name ++ "!")

fooの型はただの空タプルなので意味はない。また最後のputStrLnはmainの型を調べるのに必要らしく、<-で束縛することはできない。このことは第13章でモナドを勉強すれば理解できる。

let

<-でIOアクションの値を束縛できたのと逆に、純粋な値をIOブロックの中で束縛するにはletを使う必要がある。

main = do
    firstName <- getLine
    lastName <- getLine
    let bigFirstName = map to Upper firstName
        bigLastName = map to Upper lastName
    putStrLn $ bigFirstName ++ bigLastName

return

returnは何もしないIOを作るのに使われる。また他の言語のreturnと違って実行してもプログラムは終了されずに続行される。以下はthenとelseの型を揃えるのに使われる例。

main = do
    line <- getLine
    if null line
        then return ()
        else do
            putStrLn $ reversewords line
            main
{-
elseの部分はthenとelseの型が揃っていることを強調するために、
        else (do
            putStrLn $ reversewords line
            main)
と書くことも出来る。
-}

reversewords :: String -> String
reversewords = unwords . map reverse . words

実際はreturnは純粋な値をIOに変換するためにある関数で、

-- 等価
a <- return "hell"
let a = "hell"

のようにも使えるが普通この用途ではletを使う。

便利な関数たち

  • putStr: putStrLnと違って文末に改行を出力しない。
  • putChar: 1文字(Char)を出力する。文末に改行を出力しない。
  • print: String以外の値を表示するのに使われる。基本的にはputStrLn . showと同じでShow型のインスタンスなら何でも表示できる。
  • when: Control.Monadをインポートして利用する。条件分岐でTrueの場合にしかIOアクションがなくても良い場合に便利な関数。
import Control.Monad
main = do
    when (input == "SWORDFISH") $ do
        putStrLn input

-- 等価
main = do
    if (input == "SWORDFISH")
        then putStrLn input
        else return ()
  • sequence: IOアクションのリストをとってまとめたIOアクションを1つ返す。
> rs <- sequence [getLine, getLine]
> print rs
["something", "otherthing"]

> sequence $ map print [1,2,3]
1
2
3
[(),(),()]

2つ目の例の最後の[(),(),()]はsequenceが表示させたものではなく、ghciがIOアクションを表示した際に帰ってくる型が()以外だとその型を表示するようになっているため。

  • mapM、mapM_: IOアクションをmapするのに使う。(sequence不要)
-- 等価
> sequence $ map print [1,2]
> mapM print [1,2]
1
2
[(),()]

-- IOアクションをまとめて評価する?
> mapM_ print [1,2]
1
2
  • forever: Control.Monadをインポートして利用する。IOアクションを引数にとってそのアクションを繰り返す。main = forever $ do...
  • forM: Control.Monadをインポートして利用する。forMはリストの要素それぞれについてIOアクションを起こし、その結果をまとめたものIOアクションを返す。mapMと引数の並びが違う。
import Control.Monad
main = do
    colors <- forM [1,2,3,4] (\a -> do
        putStrLn <- show a
        color  <- getLine
        return color)
    mapM_ putStrLn colors

プログラム中のある部分だけでアクションを並べて実行したいときにdoと一緒に使う。