3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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記法

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

io.hs
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を書き直す。型宣言はそのまま、関数定義を編集する。

io.hs
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式では、値に対してパターンマッチをして、マッチするかどうかでコードを分岐させる。

あいさつする

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

case.hs
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を調べる関数の例を定義する。

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について説明する。型は、つぎのようになる。

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の先頭に、つぎのように追加する。

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引数の値を、かえす。

くりかえし

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

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を束縛する。

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_に定義したように、おなじ処理をくりかえす。

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

*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
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?