「Haskell入門ハンズオン #2」の当日用の資料(1)
この記事について
これは、Haskell入門ハンズオン #2の当日用の資料です。大いそぎで作成しているので、まちがい等、多々あるかと思います。ご指摘いただければ幸いです。
3時間で、ぎりぎり「遊べる」迷路ゲームを作るところまで説明するために、大胆に学ぶ項目を削った。Haskellにとって、とても重要な「型」の説明を削るなど、言語道断なことをしている。とりあえず、動くものを作ることで、言語の雰囲気を感じとってほしい。きちんとした説明は拙書「Haskell 教養としての関数型プログラミング」を参照していただけると幸いだ。「本当のこと」を説明することを重視し、僕の考えるベストプラクティスも随所にちりばめた。「読んで手を動かすことで、きちんと身につく一冊」になっていると自負している。
- 「Haskell入門ハンズオン #2」当日用資料(1)
- 「Haskell入門ハンズオン #2」当日用資料(2)
- 「Haskell入門ハンズオン #2」当日用資料(3)
はじめに
今日のねらい
今日のねらいは、Haskellの魅力を知ってもらうこと。そのために
- 実際にHaskellでコードを書く
- Haskellの特徴がわかる例を示す
- Haskellでなにか作ってみる
しかし
- 本当の魅力をつたえることは困難
- 時間の制約がある
- 今日の「紹介」を入口にして
本当の魅力を知るところまで学んでほしい
今日はねらわないこと
Haskellの本当の魅力はわからない。なぜなら
- Haskellの魅力のひとつは「複雑さの制御」
- 短時間で紹介できる例では実感しづらい
- 保守性の高さ
- コードを長年保守しないとわからない
- 構文によって、かくされた意味論の美しさ
- 短時間では「すっぴん」は紹介できない
- 深みに、はまる楽しさ
- マニアになるまでわからない
Haskellでは
- 「かんたんなこと」は「かんたん」に
- 「ふつうのこと」は「ふつう」に
- 「難しいこと」は「それほど難しくなく」解決できる
構文糖と実際の意味論
Haskellでは実際の意味論と、構文上の「見かけ」が異なることがある。読み書きしやすくするための構文を「構文糖」とよぶ。今回は「構文糖」でオブラートされた「見かけ」のところを説明する。オブラートを取りのぞいた意味論の話はしない。
前提
処理系は導入ずみであるという前提で話を進める。
代入と束縛
できるだけわかりやすい言葉を使いたい。「代入」という言葉のほうが「束縛」という言葉より、入門者にはわかりやすいとも思う。しかし、やはり「代入」という言葉は関数型言語の説明では使いにくい。ここでは、だいたい、つぎのように理解しておけばいい。
値123が変数xを束縛する
値123で変数xを束縛する
これらが、つぎのような意味になると思ってほしい。
値123を変数xに代入する
対話環境を使う
まずは、処理系の導入ができていることを確認する。
% stack ghc -- --version
The Glorious Glasgow Haskell Compilation System, version 8.X.X
%はシェルへの入力をあらわす記号(プロンプト)なので、%そのものは入力しない。上記のように表示されれば処理系が導入されていることになる。
対話環境を試す
対話環境を試してみよう。
% stack ghci
(いくつかのメッセージ)
Prelude>
プロンプトにあるPreludeが何なのかは、ここでは説明しない。また、今後はプロンプトは>のみで示すことにする。文字を打ち込んでみよう。
> 'c'
'c'
対話環境に打ち込まれた「式」は「評価」されて、つぎの行に「表示」される。式'c'が評価されて、そのまま、値'c'になり、それが表示されて'c'のように出力される。整数やたし算を打ち込んでみよう。
> 123
123
> 123 + 456
579
式123は値123に評価され、式123 + 456は値579に評価される。値で変数を束縛することもできる。
> x = 123
> x
123
関数を定義することもできる。
> bmi h w = w / (h / 100) ^ 2
> bmi 172 68
22.985397512168742
これはBMIをもとめる関数だ。体重w(kg)を身長(cm)の100分の1の2乗でわったものがBMIになる。BMIは22くらいが調度いい。対話環境を終わらせるには、つぎのようにする。
> :quit
対話環境のなかで定義した関数は、対話環境を閉じれば消えてしまう。
定義ファイルを読み込む
ファイルに保存した関数を対話環境に読み込んでみよう。まず、ファイルbmi.hsを作成する。
% vim bmi.hs
bmi h w = w / (h / 100) ^ 2
例としてVimとしたが、お気に入りのエディタを使おう。定義ファイルを読み込むには:loadコマンドを使う。対話環境を立ち上げて、つぎのように打ち込む。
> :load bmi.hs
> bmi 172 68
22.985397512168742
定義ファイルを編集したら「再読み込み」する必要がある。試してみよう。対話環境とは、べつのウィンドウで、エディタを開こう。bmi.hsに、つぎの関数を追加する。
% vim bmi.hs
isObese h w = bmi h w >= 25
開いたままにしてある対話環境で:reloadコマンドを試す。
> :reload
> isObese 172 68
False
> isObese 172 80
True
BMI値が25以上で肥満とされる。肥満であるかどうかを判定する関数isObeseを追加した。
関数、再帰、パターンマッチ
関数とは
関数とは「値を値に関連づけるもの」だ。たとえば、たし算なら整数値3と4を整数値7に関連づける。
> 3 + 4
7
演算子(+)による、たし算の例だ。演算子は中置記法により、ふたつの引数をとる関数だ。丸括弧でかこむことで、前置記法とすることもできる。
> (+) 3 4
7
整数値3と4とが、それぞれ、第1引数、第2引数になる。円の面積をもとめる関数を作ってみよう。ファイルcircleArea.hsを、つぎような内容で作成する。
circleArea r = r ^ 2 * pi
対話環境に読み込んで試してみよう。
> :load circleArea.hs
> circleArea 3
28.274333882308138
値piは定義ずみの変数であり、円周率を示す。関数の定義は、つぎのような構文になる。
[関数名] [仮引数1] [仮引数2] ... = [式]
再帰
ほかの言語では「くりかえし」が重要な役割を持つ。Haskellでは「くりかえし」をあらわす構文はない。「くりかえし」ではなく、より強力な「再帰」を使う。「再帰」とはなにか?
あるものについて記述するとき
記述しているもの、それ自身への参照が
その記述中にあらわれること
1からnまでの総和を計算する関数は、つぎのようになる。
sumN 1 = 1
sumN n = sumN (n - 1) + n
1から1までの総和は1
1からnまでの総和は
1からn - 1までの総和にnをたしたもの
対話環境で試してみよう。
> sumN 1 = 1; sumN n = sumN (n - 1) + n
> sumN 10
55
Maybe値
値を持たないかもしれない値としてMaybe値がある。Maybe値にはNothing値とJust値とがある。
> Just 8
Just 8
> Nothing
Nothing
わる数が0のときに値をもたないということを明示した、わり算を示す。
> :{
| divide _ 0 = Nothing
| divide a b = Just (a `div` b)
| :}
> divide 13 3
Just 4
> divide 8 0
Nothing
:{と:}とを使うと、対話環境で複数行での定義ができる。_(アンダースコア)は、仮引数のかわりに置くことができ、不要な変数を定義せずにすむ。関数divは整数どうしのわり算をあらわす。
パターンマッチ
Just値になってしまった値をどう使おうか。
> :{
| maybeMul (Just a) b = Just (a * b)
| maybeMul Nothing _ = Nothing
| :}
> maybeMul (divide 13 3) 5
Just 20
関数maybeMulの、ひとつめの仮引数は、つぎのふたつである。
Just a
Nothing
これらは適用された実引数と比較される。Just aとJust 4が比較されると、変数aは4に束縛される。これをパターンマッチと呼ぶ。関数maybeMulの第1引数にNothingがあたえられたとする。すると、まず、Just aと比較されるが、これはかたちが異なるため、マッチしない。つぎに、仮引数のNothingと比較される。これはかたちがおなじなのでマッチする。
Maybe値を例に、パターンマッチを学んだ。仮引数のところにはパターンが置ける。実引数はパターンと比較される。かたちがおなじなら、なかの値を取り出せる。かたちがちがければ、つぎの定義へと進む。マッチするまで、つぎつぎに試される。
ここまで見てきたパターンは、つぎの3種類だ。
- 変数(x, y, z, ...)
- ワイルドカード(_)
- 値構築子が0個以上の引数をとったもの(Just x, Nothing, ...)
仮引数である「変数」そのものもパターンのひとつと考えられる。
タプル、リスト、case式
タプル
複数の値をセットにして、ひとつの値にしたい?タプルを使っては、いかがでしょうか。
> ("Yoshikuni Jujo", 37)
("Yoshikuni Jujo", 37)
> yj = it
変数itで直前に評価された値を取り出せる。タプルのなかみを取り出すにはパターンマッチを使う。
> :{
| showPerson (n, a) =
| n ++ " (" ++ show a ++ ")"
> :}
> showPerson yj
"Yoshikuni Jujo (37)"
タプルを使うと、複数の値をまとめて、ひとつの値にできる。丸括弧でかこんで、値どうしをカンマでくぎる。
("foo", 123)
なかみを取り出すにはパターンマッチを使う。
bar (x, y) = ...
リスト
Haskellでよく使われるデータ構造としてリストがある。0個以上のおなじタイプのデータを順番に並べたものだ。対話環境でみてみよう。
> [3, 2, 8, 5, 4]
[3,2,8,5,4]
リストの先頭に要素を追加するには、演算子(:)を使う。
> 3 : [2, 8, 5, 4]
[3,2,8,5,4]
> 3 : 2 : [8, 5, 4]
[3,2,8,5,4]
> 3 : 2 : 8 : 5 : 4 : []
[3,2,8,5,4]
最後の入力では、空リストの先頭に4, 5, 8, 2, 3の順で、要素をつぎつぎに追加している。結果として[3,2,8,5,4]になる。そのようにみえる。しかし実は、[3,2,8,5,4]のような表示は「わかりやすさ」のためにある。実際には3 : (2 : (8 : (5 : (4 : []))))として保存されていると考えていい。なので、x : xsのようなパターンでマッチすると、先頭(3)が変数xを、残り(8 : (5 : ...))が変数xsを束縛する。つまり、パターンマッチによって、「先頭と残り」にわけることができる。
リストをあつかう関数
先頭の値と残りのリストからなるタプルを作る関数。空リストのときは値を持たない。
headTail [] = Nothing
headTail (x : xs) = Just (x, xs)
対話環境で試してみよう。
> :{
| headTail [] = Nothing
| headTail (x : xs) = Just (x, xs)
| :}
> headTail [3, 2, 8, 5, 4]
Just (3,[2,8,5,4])
リストに対しても再帰関数が定義できる。整数のリストの、すべての値をたしあわせる関数は、つぎのようになる。
mySum [] = 0
mySum (x : xs) = x + mySum xs
空リストの総和は0だ。リストxsの先頭に整数xを追加したリストの総和は、整数値xとリストxsの総和とを、たし算した値になる。対話環境で試す。
> :{
| mySum [] = 0
| mySum (x : xs) = x + mySum xs
| :}
> mySum [1, 2, 3, 4, 5]
15
もとのリストの要素をすべて2倍したリストを作る関数は、つぎのようになる。
toDoubleAll [] = []
toDoubleAll (x : xs) = x * 2 : toDoubleAll xs
空リストのすべての要素を2倍したリストは空リストである。リストxsの先頭に整数値xを追加したリストの要素のすべてを2倍したリストは、リストxsのすべての要素を2倍したリストの先頭に整数値xを2倍した値を追加したものだ。対話環境で試す。
> :{
| toDoubleAll [] = []
| toDoubleAll (x : xs) = x * 2 : toDoubleAll xs
| :}
> toDoubleAll [1, 2, 3, 4, 5]
[2,4,6,8,10]
もとのリストの要素のうち奇数のみを取り出したリストを作る関数を考える。奇数であることを確認する関数oddについて、まずはみてみよう。
> odd 8
False
> odd 13
True
値FalseやTrueは、Bool値であり、値Falseが「そうでない」を値Trueが「そうである」をあらわす。つぎに、関数boolについてみてみよう。関数boolは値FalseやTrueに対して、それぞれの値をかえす関数だ。
> :module Data.Bool
> bool "BOO" "GOOD" False
"BOO"
> bool "BOO" "GOOD" True
"GOOD"
コマンド:moduleでモジュールData.Boolを導入した。これで、関数boolが導入される。関数boolの使いかたは、つぎのようになる。
bool [Falseのときの値] [Trueのときの値] [Bool値]
関数oddとboolを組み合わせると、奇数のときと偶数のときとで、それぞれの値をかえすことができる。
> bool "It isn't odd." "It's odd!" (odd 8)
"It isn't odd."
> bool "It isn't odd." "It's odd!" (odd 13)
"It's odd!"
リストのなかから奇数だけ取り出す関数onlyOddは、つぎのようになる。
onlyOdd [] = []
onlyOdd (x : xs) =
bool (onlyOdd xs) (x : onlyOdd xs) (odd x)
空リストから奇数だけ取り出したリストは空リストだ。リストxsの先頭に整数値xを追加したリストから奇数だけを取り出したリストは、xが奇数ではないとき、リストxsから奇数のみ取り出したリストになり、xが奇数のとき、リストxsから奇数のみ取り出したリストに値xを追加したものになる。ファイルlistRec.hsを、つぎのような内容で作成する。
onlyOdd [] = []
onlyOdd (x : xs) =
bool (onlyOdd xs) (x : onlyOdd xs) (odd x)
対話環境で試す。
> :load listRec.hs
> onlyOdd [1, 2, 3, 4, 5]
[1,3,5]
where節
関数onlyOddの定義では、関数boolの引数に、onlyOdd xsが2回、使われている。これはDRY(Don't Repeat Yourself)原則に反している。ファイルlistRec.hsの定義を、where節を使って書き換えよう。
onlyOdd [] = []
onlyOdd (x : xs) = bool xs' (x : xs') (odd x)
where xs' = onlyOdd xs
where節を使ってローカル変数xs'を定義した。対話環境で試してみよう。
> :reload
> onlyOdd [1, 2, 3, 4, 5]
[1,3,5]
case式
ここまでは関数定義の仮引数のところでのパターンマッチだけをみてきた。もっと自由にパターンマッチを使いたいことがある。文字を小文字にしたうえで、パターンマッチさせたい。ファイルuseCase.hsをつぎのように作成する。
import Data.Char
answer c = case toLower c of
'y' -> Just True
'n' -> Just False
_ -> Nothing
モジュールData.Charの関数toLowerは小文字化する関数だ。予約語caseとofのあいだの式がパターンマッチの対象になる。それぞれの行で'y'や'n'、そして_とマッチが試される。文字リテラルも「パターン」になることに注意しよう。Haskellでは、たいていのリテラルが「パターン」として使える。試してみよう。
> :load useCase.hs
> answer 'y'
Just True
> answer 'N'
Just False
> answer 'c'
Nothing
case式の記法は、つぎのようになる。
case [式] of
[パターン1] -> [式1]
[パターン2] -> [式2]
...
[パターン] -> [式]の行は、予約語caseを含む行より深くインデントする。それぞれの[パターン] -> [式]の行は、インデントをそろえる。インデントには「タブを使わない」ことが奨励されているが、「僕は使う」。「タブを使う」ときにはタブは8タブとされる。