Haskell入門ハンズオン! #3: 当日用資料 5/5
はじめに
Haskellの「関数」にできるのは、「引数をとり、返り値をかえすこと」だけだ。ここまでは、対話環境で返り値を表示してきた。そろそろ、「対話環境で」ではなく、独立した実行可能ファイルを作りたい。しかし、「関数」にはキー入力を受け取ることも、標準出力に出力することもできない。どうしたらいいだろうか?
入出力という値
Haskellでは、たとえば数値や文字列という値とおなじように、「入出力」という値がある。「入出力」という値は、より細かい「入出力」という部品から組み立てられる。組み立てられた「入出力」は、対話環境で評価されたとき、あるいは、変数mainを束縛したものは、スタンドアロンなプログラムのなかで、実行される。
1行を表示
まずは1行を標準出力に出力する。
*Main> putStrLn "hello"
hello
まちがいやすいところだが、関数の評価の副作用として出力されたのではない。対話環境は、「式を評価した結果」が「入出力」の場合、その入出力を「実行」してくれる。
...のつぎに...
「入出力」は値なので演算子によって演算できる。「入出力」に対して(は)、「...のつぎに...」を意味する演算子(>>)がある。
*Main> putStrLn "hello" >> putStrLn "world"
hello
world
...の結果をわたして...
「入出力」の「入力」のところはどうだろうか。「入力」は「入力された値」が使えないと意味がない。Haskellでは「入力された値」を、つぎの「入出力」にわたす仕組みがある。それには、演算子(>>=)を使う。
*Main> getLine >>= putStrLn
(なにか入力し改行)hello
hello
入出力の型
「入出力」も値なので型がある。
*Main> :type getLine
getLine :: IO String
「入出力」の型はIO aであり、型変数aのところは、つぎの入出力にわたす値の型だ。つぎにわたす値がないときは、型変数aのところは「情報がない」値の型であるユニット型になる。
*Main> :type putStrLn "hello"
putStrLn "hello" :: IO ()
do記法
ここまでみてきた「入出力」の例をファイルに定義する。
hello :: IO ()
hello = putStrLn "hello" >> putStrLn "world"
echo :: IO ()
echo = getLine >>= putStrLn
対話環境で試してみる。
*Main> :load io.hs
*Main> hello
hello
world
*Main> echo
(なにか打ち込み、改行)hello
hello
Haskellにはdo記法という構文糖がある。do記法を使って、入出力hello, echoを書き直す。型宣言はそのまま、関数定義を編集する。
hello = do
putStrLn "hello"
putStrLn "world"
echo = do
l <- getLine
putStrLn l
do記法を使うと、ふつうの手続き型言語のように書ける。対話環境で試してみよう。
*Main> :reload
*Main> hello
hello
world
*Main> echo
(なにか打ち込み、改行)hello
hello
case式
ここで、case式を紹介する。Haskellでコードを分岐させるには、いくつかの構文があるが、case式がすべての基礎になっている。case式では、値に対してパターンマッチをして、マッチするかどうかでコードを分岐させる。
あいさつする
特定の人にだけ、ていねいにあいさつする関数を定義する。
helloTo :: String -> String
helloTo n = case n of
"Yoshikuni" -> "Good morning, sir."
_ -> "Hello, " ++ n ++ "!"
予約語caseとofのあいだに式を置く。その式の評価された結果を、予約語->の左側のパターンとマッチさせる。最初にマッチしたパターンの右側の式が全体の結果になる。試してみる。
*Main> :load case.hs
*Main> helloTo "Yoshikuni"
"Good morning, sir."
*Main> helloTo "Ichiro"
"Hello, Ichiro!"
Yoshikuniにだけ、ていねいにあいさつしている。
ユーザのIDを調べる
ユーザのIDを調べる関数の例を定義する。
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について説明する。型は、つぎのようになる。
lookup :: a -> [(a, b)] -> Maybe b
タプルのリストを辞書として、タプルの第1要素で検索し、検索が成功すれば第2要素をJust値としてかえし、失敗すればNothing値をかえす。
関数lookupの結果に対して、case式を使用し、Just値とNothing値とで処理をわけている。試してみる。
*Main> :reload
*Main> getId "Keiko"
"ID: 5"
*Main> getId "Yoshio"
"No such user"
電卓
さて、ここまで対話環境で試してきた電卓を、スタンドアロンなプログラムにしていこう。ターミナルに打ち込んだ式を評価して表示する。これを、くりかえすようにする。
モジュールから関数を導入する
まずは、必要な関数などを既存のモジュールから導入する。ファイルcalc.hsの先頭に、つぎのように追加する。
import System.IO (hFlush, stdout)
import Data.Bool (bool)
関数boolを試してみよう。
*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引数の値を、かえす。
くりかえし
つぎの「入出力」にブール値をわたす「入出力」を引数として、その「入出力」をくりかえす「入出力」を作る関数、を定義する。
doWhile_ :: IO Bool -> IO ()
doWhile act = do
c <- act
bool (return ()) (doWhile_ act) c
わたされるブール値で変数cを束縛し、そのブール値の値によって、値Falseならreturn ()とし、値TrueならdoWhile_ actを再帰的に呼び出す。return ()はユニット値を、つぎの入出力にわたすだけで「なにもしない」。
動作main
電卓の動作を行う「入出力」で変数mainを束縛する。
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_に定義したように、おなじ処理をくりかえす。
対話環境で試してみよう。
*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