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

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

はじめに
------

Haskellの「関数」にできるのは、「引数をとり、返り値をかえすこと」だけだ。ここまでは、対話環境で返り値を表示してきた。そろそろ、「対話環境で」ではなく、独立した実行可能ファイルを作りたい。しかし、「関数」にはキー入力を受け取ることも、標準出力に出力することもできない。どうしたらいいだろうか?

入出力という値
-----------

Haskellでは、たとえば数値や文字列という値とおなじように、「入出力」という値がある。「入出力」という値は、より細かい「入出力」という部品から組み立てられる。組み立てられた「入出力」は、対話環境で評価されたとき、あるいは、変数mainを束縛したものは、スタンドアロンなプログラムのなかで、実行される。

1行を表示
-------

まずは1行を標準出力に出力する。

```hs
*Main> putStrLn "hello"
hello
```

まちがいやすいところだが、関数の評価の副作用として出力されたのではない。対話環境は、「式を評価した結果」が「入出力」の場合、その入出力を「実行」してくれる。

...のつぎに...
------------

「入出力」は値なので演算子によって演算できる。「入出力」に対して(は)、「...のつぎに...」を意味する演算子(>>)がある。

```hs
*Main> putStrLn "hello" >> putStrLn "world"
hello
world
```

...の結果をわたして...
-------------------

「入出力」の「入力」のところはどうだろうか。「入力」は「入力された値」が使えないと意味がない。Haskellでは「入力された値」を、つぎの「入出力」にわたす仕組みがある。それには、演算子(>>=)を使う。

```hs
*Main> getLine >>= putStrLn
(なにか入力し改行)hello
hello
```

入出力の型
--------

「入出力」も値なので型がある。

```hs
*Main> :type getLine
getLine :: IO String
```

「入出力」の型はIO aであり、型変数aのところは、つぎの入出力にわたす値の型だ。つぎにわたす値がないときは、型変数aのところは「情報がない」値の型であるユニット型になる。

```hs
*Main> :type putStrLn "hello"
putStrLn "hello" :: IO ()
```

do記法
------

ここまでみてきた「入出力」の例をファイルに定義する。

```hs:io.hs
hello :: IO ()
hello = putStrLn "hello" >> putStrLn "world"

echo :: IO ()
echo = getLine >>= putStrLn
```

対話環境で試してみる。

```hs
*Main> :load io.hs
*Main> hello
hello
world
*Main> echo
(なにか打ち込み、改行)hello
hello
```

Haskellにはdo記法という構文糖がある。do記法を使って、入出力hello, echoを書き直す。型宣言はそのまま、関数定義を編集する。

```hs:io.hs
hello = do
        putStrLn "hello"
        putStrLn "world"

echo = do
        l <- getLine
        putStrLn l
```

do記法を使うと、ふつうの手続き型言語のように書ける。対話環境で試してみよう。

```hs
*Main> :reload
*Main> hello
hello
world
*Main> echo
(なにか打ち込み、改行)hello
hello
```

case式
------

ここで、case式を紹介する。Haskellでコードを分岐させるには、いくつかの構文があるが、case式がすべての基礎になっている。case式では、値に対してパターンマッチをして、マッチするかどうかでコードを分岐させる。

### あいさつする

特定の人にだけ、ていねいにあいさつする関数を定義する。

```hs:case.hs
helloTo :: String -> String
helloTo n = case n of
        "Yoshikuni" -> "Good morning, sir."
        _ -> "Hello, " ++ n ++ "!"
```

予約語caseとofのあいだに式を置く。その式の評価された結果を、予約語->の左側のパターンとマッチさせる。最初にマッチしたパターンの右側の式が全体の結果になる。試してみる。

```hs
*Main> :load case.hs
*Main> helloTo "Yoshikuni"
"Good morning, sir."
*Main> helloTo "Ichiro"
"Hello, Ichiro!"
```

Yoshikuniにだけ、ていねいにあいさつしている。

### ユーザのIDを調べる

ユーザのIDを調べる関数の例を定義する。

```hs:case.hs
users :: [(String, Int)]
users = [
        ("Taro", 3),
        ("Saburo", 9),
        ("Keiko", 5) ]

getId :: String -> String
getId n = case lookup n users of
        Just i -> "ID: " ++ show i
        Nothing ->"No such user"
```

関数lookupについて説明する。型は、つぎのようになる。

```hs
lookup :: a -> [(a, b)] -> Maybe b
```

タプルのリストを辞書として、タプルの第1要素で検索し、検索が成功すれば第2要素をJust値としてかえし、失敗すればNothing値をかえす。

関数lookupの結果に対して、case式を使用し、Just値とNothing値とで処理をわけている。試してみる。

```hs
*Main> :reload
*Main> getId "Keiko"
"ID: 5"
*Main> getId "Yoshio"
"No such user"
```

電卓
----

さて、ここまで対話環境で試してきた電卓を、スタンドアロンなプログラムにしていこう。ターミナルに打ち込んだ式を評価して表示する。これを、くりかえすようにする。

### モジュールから関数を導入する

まずは、必要な関数などを既存のモジュールから導入する。ファイルcalc.hsの先頭に、つぎのように追加する。

```hs:calc.hs
import System.IO (hFlush, stdout)
import Data.Bool (bool)
```

関数boolを試してみよう。

```hs
*Main> :load calc.hs
*Main> bool "else" "then" False
"else"
*Main> bool "else" "then" True
"then"
*Main> bool "odd" "even" (even 8)
"even"
```

第3引数のブール値が値Falseなら第1引数の値を、値Trueなら第2引数の値を、かえす。

### くりかえし

つぎの「入出力」にブール値をわたす「入出力」を引数として、その「入出力」をくりかえす「入出力」を作る関数、を定義する。

```hs:calc.hs
doWhile_ :: IO Bool -> IO ()
doWhile act = do
        c <- act
        bool (return ()) (doWhile_ act) c
```

わたされるブール値で変数cを束縛し、そのブール値の値によって、値Falseならreturn ()とし、値TrueならdoWhile_ actを再帰的に呼び出す。return ()はユニット値を、つぎの入出力にわたすだけで「なにもしない」。

### 動作main

電卓の動作を行う「入出力」で変数mainを束縛する。

```hs:calc.hs
main :: IO ()
main = doWhile_ $ do
        putStr "> "
        hFlush stdout
        l <- getLine
        case l of
                ":q" -> return False
                _ -> do case calc l of
                        Just n -> print n
                        Nothing -> putStrLn "parse error"
                        return True
```

doWhile_ \$ doの演算子(\$)は、do以降の「入出力」が関数doWhile_の引数であることをしめす。putStr "> "のあとにあるhFlush stdoutは、改行を待たずに表示するためにバッファの内容を出力させる。入出力getLineがわたす値lについて、":q"であればreturn Falseによって終了になる。そうでなければ、関数calcで数式として評価する。構文エラーかどうかで処理をわけ、構文エラーでなければ結果の値を表示、構文エラーならばエラーメッセージを表示する。そして、return Trueとすることで、関数doWhile_に定義したように、おなじ処理をくりかえす。

対話環境で試してみよう。

```hs
*Main> :reload
*Main> main
> 3+5
8
> (11-9)*2
4
> (21*3)/(7+4)
5
> :q
*Main>
```

### 実行可能ファイル

Haskellで実行可能ファイルを作るには、実行したい入出力で、特別な変数mainを束縛する。ファイルcalc.hsでは、すでに変数mainは定義ずみなので、つぎのようにして、実行可能ファイルを作る。

    % stack ghc -- calc.hs -o calc

タブ文字に対する警告を消したければ

    % stack ghc -- calc.hs -o calc -fno-warn-tabs

試してみよう。

    % ./calc
    > 3+5
    8
    > (3+5)*9
    72
    > ((3+5)*9)/(3+4)
    10
    > :q

まとめ
-----

打ち込んだ式を評価する電卓を作った。作りながら、そのなかで使われている構文や技法を学んだ。

質問など
------

質問などはteratailでHaskellタグをつけていただければ、解答します。またSlackでの質問や、メールでの質問も大歓迎です。

### teratail

teratailでは、Haskellタグをつけて質問してください。

### Slack

Slackのアドレスは、つぎのとおりです。

    https://haskelldojo.slack.com

はじめてのかたは、つぎのアドレスから登録をお願いします。

    http://haskelldojo.herokuapp.com

### メール

メールで質問したいかたは、つぎのアドレスまで。よろしくおねがいします。

    funpaala@gmail.com