Haskell入門ハンズオン! #5 - 当日用資料 (3/5)

Haskell入門ハンズオン! #5 - 当日用資料 (3/5)
========================================================================

IOモナド
-------

Haskellでは入出力や状態変化をともなう処理は、IOモナドというかたちで、あつかわれる。「モナド」という難しげな言葉に、おどろく必要はない。理解すれば、どうということはない概念だ。圏論とかを理解する必要はない。さまざまな処理を抽象化してくれる便利な道具だ。今回は「モナド」については、あまりふれない。IOモナドを「組み立て」という側面から説明する。

手続き
-----

つぎのような「手続き」をみてみよう。

```rb
puts "hello"
s = gets
puts s
```
これは、いちおうRubyの例だ。puts "hello"をしたあとに、getsをして、キーボード入力を変数sに代入し、その代入された値sをputsしている。この記法では、「AのつぎにBをする」という「関係」が上下の位置によって暗黙に示されている。

Rubyなどでは「暗黙に」示されている関係が、Haskellでは明示的に演算子になる。

```hs
putStrLn "hello" >>
getLine >>=
\s -> putStrLn s
```

「AのつぎにBをする」はA >> Bと表される。「Cの結果を利用してDをする」はC >>= Dだ。ここでDは「Cの結果」を引数とする関数になる。

do記法
-----

Haskellでは入出力は、演算子(>>)や(>>=)によって「明示的に」組み合わされる。これは、Haskellの美しさでもあり、コードを安全に、きれいに書ける性質でもある。ただ、上から下に流れる処理というのは、メタファとして、わかりやすい。Haskellでは「美しさ」「安全性」を犠牲にすることなく、あたかも、暗黙の前後関係をもつような書きかたができる。それがdo記法だ。前述のコードを再掲する。

```hs
putStrLn "hello" >>
getLine >>=
\s -> putStrLn s
```

これをdo記法を使って書くと、つぎのようになる。

```
do      putStrLn "hello"
        s <- getLine
        putStrLn s
```

今回は、時間がないので、このdo記法のかたちだけ覚えておこう。そのうらがわに、明示的な演算子による結合があると、知っておけばいい。do記法で入出力処理を書いてみよう。

```
% vim samples/do.hs
```

```hs:samples/do.hs
io1 :: IO ()
io1 = do
        putStrLn "hello"
        s <- getLine
        putStrLn s
```

IOモナドの型は、入力される値がなければ、つぎのようになる。

```
IO()
```

変数io1を、「do記法によって組み立てた入出力」を表す値で束縛した。試してみる。

```
> :load samples/do.hs
> io1
hello
(適当に入力してエンター)world
world
```

do記法のなかで使える予約語letを使った構文がある。

```
% vim samples/do.hs
```

```hs:samples/do.hs
io2 :: IO ()
io2 = do
        n <- getLine
        let     msg = "Hello, " ++ n ++ "!"
        putStrLn msg
```

このように、do記法のなかで変数を定義できる。試してみる。

```
> :reload
> io2
(名前を入力)Yoshio
Hello, Yoshio!
```

標準入出力
--------

標準入出力への入出力について学ぶ。標準入力はデフォルトではキーボードからの入力。標準出力はデフォルトでは画面への出力になる。このふたつは、すでに出てきたが、もういちど確認する。対話環境で試してみる。

```
> putStrLn "hello"
hello
> getLine
(適当に入力)world
"world"
```

関数putStrLnは文字列を引数にとり、その文字列を標準出力に出力する。入出力getLineは標準入力から1行入力し、それを「つぎの」入出力にわたす。型をみてみよう。

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

つぎにわたす値がないとき、入出力の型は、つぎのようになる。

```
IO ()
```

つぎにわたすのがString型の値のとき、つぎのようになる。

```
IO String
```

改行を追加しないで出力するには、つぎのようにする。

```
> putStr "hello"
hello>
```

このように、関数putStrを使う。文字列以外の値を表示するには関数printを使う。

```
> print 123
123
```

関数printは、関数showによって文字列化したものを、関数putStrLnによって表示するのと、おなじことだ。

```
> putStrLn (show 123)
123
```

ファイル入出力
-----------

つぎに、ファイルへの入出力をみてみよう。

```
> writeFile "tmp_hnh.txt" "Haskell Handson!\n"
> readFile "tmp_hnh.txt"
"Haskell Handson!\n"
```

writeFileは第1引数のファイルに、第2引数の文字列を書き込む。readFileは第1引数のファイルの内容を読み込む。

現在時刻
-------

現在の時刻を取得する。時刻は入力値として取得される。

```
> :module Data.Time
> getCurrentTime
2018-10-15 05:54:10.471924992 UTC
```

:moduleコマンドでモジュールData.Timeを、対話環境に導入している。入出力getCurrentTimeによって現在時刻が得られる。入出力getCurrentTimeの型は、つぎのようになる。

```
> :type getCurrentTime
getCurrentTime :: IO UTCTime
```

分岐
----

順接、分岐、反復の3つで、すべてのアルゴリズムが記述可能だ。「順接」は、「AのつぎにB」ということだ。すでにみた。つぎは「分岐」をみてみよう。

標準出力に表示するか、ファイルに保存するこわ選ぶ例だ。つぎのようなコードを書く。

```
% vim samples/currentTime.hs
```

```hs:samples/currentTime.hs
import Data.Time

currentTime :: IO ()
currentTime = do
        putStrLn "現在時刻を取得します"
        now <- getCurrentTime
        putStrLn "ファイルに保存しますか?(Y/N)"
        s <- getLine
        if s == "Y" || s == 'y'
                then writeFile "currentTime.txt"
                        (show now ++ "\n")
                else putStrLn (show now)
```

if式を使って分岐を記述することができた。試してみよう。

```
> :load samples/currentTime.hs
> currentTime
現在時刻を取得します
ファイルに保存しますか?(Y/N)
(nを入力)n
2018-11-21 04:34:15.538628867 UTC
```

反復
---

反復、つまり「くりかえし」は再帰で書くことができる。

```
% vim samples/sum.hs
```

```hs:sampls/sum.hs
total :: Integer -> IO Integer
total s = do
        ln <- getLine
        let     n = read ln
        if n < 0
                then return s
                else total (s + n)
```

入力値が0より小さければ、sの値をかえし、そうでなければ、s + nを引数に自身を呼び出す。試してみる。

```
> :load samples/sum.hs
> total 0
3
24
11
- 1
38
```

3, 24, 11を入力し、終了させるために- 1を入力した。3 + 24 + 11 = 38なので、38が表示される。

独立して実行できるプログラム
----------------------

ここまで、対話環境で試してきた。ここで、コンパイルして独立実行できる実行可能形式の作りかたを示す。Haskellでは、変数mainを束縛した入出力が実行される。sum.hsに入出力mainを追加する。

```
% vim samples/sum.hs
```

```hs:samples/sum.hs
main :: IO ()
main = do
        s <- total 0
        print s
```

つぎのようにしてコンパイルして、実行する。

```
% stack ghc -- samples/sum.hs -o sum
% ./sum
3
24
11
-1
38
```

IOモナドのまとめ
-------------

Haskellでは入出力や状態変化をともなう処理には、IOモナドという仕組みが使われる。IOモナドはモナドの一種だが、ここでは「モナド」については説明しない。手続き型言語では「暗黙に」されていた「組み立て」が、明示的な「演算子」によって、おこなわれる。手続き型言語のような書きかたのできるdo記法がある。順接、分岐、反復の実装をみた。Haskellでは、コンパイルされた実行可能形式は、変数mainを束縛した入出力を実行する。

追加の知識
--------

このあと、プロジェクト例を示すが、そのために必要な追加の知識を、ここで学ぶ。まずは型Intを紹介する。つぎに、Haskellにおいて、かつては、標準的だった乱数生成ライブラリについて学ぶ。

整数型Int
--------

Haskellには多倍長整数Integerのほかに、固定長整数Intがある。Int型の値は処理系依存の最小値、最大値をもつ。

```
> minBound :: Int
-9223372036854775808
> maxBound :: Int
9223372036854775807
```

minBound, maxBoundは多相的な値であり、それぞれの型について、その最小値、最大値を示す。うえの例は64ビットOSでの値であり、32ビットOSでは異なった値になる。

乱数生成
------

かつて標準的だったモジュールSystem.Randomはパッケージrandomに含まれている。

```
> :module System.Random
> randomIO :: IO Integer
7070170094128852650
> randomIO :: IO Double
0.19573673642621692
```

毎回ちがう乱数値を得るには、「なんらかの状態変化」が必要なので、randomIOは「入出力」になっている。randomIOは多相的なので、型注釈で型を指定している。randomIOで乱数値を生成するには、生成される値の型が、型クラスRandomのインスタンスである必要がある。型クラスRandomは、だいたい、つぎのように定義されている。

```hs
class Random a where
        randomR :: RandomGen g =>
                (a, a) -> g -> (a, g)
        random :: RandomGen g => g -> (a, g)
```

RandomGen gは型クラス制約であり、型gが型クラスRandomGenのインスタンスであることを示している。型gが型クラスRandomGenのインスタンスならば、型gにたいして、つぎのような関数が存在する。

```hs
next :: g -> (Int, g)
```

これは、g型の値が「乱数の種」であり、関数nextによって、Int型の乱数値と新しい「乱数の種(g型の値)」とが生成される、ということだ。

じゃんけん型
----------

じゃんけん型を定義する。

```
% vim samples/janken.hs
```

```hs:samples/janken.hs
import System.Random

data Janken = Rock | Paper | Scissors
        deriving Show
```

じゃんけん型を型クラスRandomのインスタンスにする。

```
% vim samples/janken.hs
```

```hs:samples/janken.hs
instance Random Janken where
        randomR = undefined
        random g = let (n, g') = next g in
                case n `mod` 3 of
                        0 -> (Rock, g')
                        1 -> (Paper, g')
                        2 -> (Scissors, g')
                        _ -> error "never occur"
```

関数randomRは「範囲を指定して乱数値を得る」関数だ。いまは使わないので、とりあえずundefinedにしておく。関数randomは「乱数の種」から、乱数値と新しい種をつくる。関数nextでInt型の値をつくり、それを3通りの値に変換している。試してみる。

```
> :load samples/janken.hs
> randomIO :: IO Janken
Scissors
> randomIO :: IO Janken
Paper
> randomIO :: IO Janken
Scissors
> randomIO :: IO Janken
Rock
```

バッファリング
-----------

標準入出力はバッファリングされている。標準入出力のバッファリングのモードを表示する。

```
% vim samples/buffer.hs
```

```hs:samples/buffer.hs
import System.IO

main :: IO ()
main = do
        bi <- hGetBuffering stdin
        bo <- hGetBuffering stdout
        print bi
        print bo
```

コンパイルして実行する。

```
% stack ghc -- samples/buffer.hs -o buffer
% ./buffer
LineBuffering
LineBuffering
```

標準入力、標準出力ともにLineBufferingだ。つまり、改行が入力(出力)されるまで入力(出力)はバッファリングされる。標準出力への出力は改行の出力まで、おくらされる。これは、たとえば「質問」の出力のあとで、改行したくないようなとき、こまる。

```
% vim samples/yesno0.hs
```

```hs:samples/yesno0.hs
main :: IO ()
main = do
        putStr "どうする?(Y/N)"
        a <- getLine
        putStrLn $ "Your answer is " ++ a ++ "."
```

コンパイルして実行する。

```
% stack ghc -- samples/yesno0.hs -o yesno
% ./yesno
(入力)foo
どうする?(Y/N): Your answer is foo.
```

質問が表示されるまえに、入力が必要になる。バッファをフラッシュする必要がある。yesno.hsの先頭にimport文を追加する。

```
% vim samples/yesno1.hs
```

```hs:samples/yesno1.hs
import System.IO
```

質問文の表示のあとにhFlushを追加する。

```
% vim samples/yesno1.hs
```

```hs:samples/yesno1.hs
...
        putStr "どうする?(Y/N): "
        hFlush stdout
...
```

コンパイルして実行する。

```
% stack ghc -- samples/yesno1.hs -o yesno
% ./yesno
どうする?(Y/N): (入力)Y
Your answer is Y.
```

hFlushをすることで、改行を待たずに表示される。

[その4へ](https://qiita.com/YoshikuniJujo/items/9b6bec1aa74ccf36bdef)