はじめに
Haskell学習中なのですが、アウトプットして理解を深めようと書いてみました。
まとめのベースは「すごいHaskell楽しく学ぼう」という本で、コード例も似ている(あるいは同じ)ものが多いです。
本の章ごとに個人メモという形で(リファレンス的な)記事を書いていこうと思います。
シリーズ :: [Qiita記事]
Haskell個人メモ :: 1.基本 ←いまここ
Haskell個人メモ :: 2.型
Haskell個人メモ :: 3.関数の構文
Haskell個人メモ :: 4.再帰
Haskellの特徴
- 純粋関数型言語(副作用を持たない)
- 静的型付け
- 型推論がある
- 遅延評価
論理演算 - && || not
True && True -- True
False || True -- True
not True -- False
否定は!
ではなくnot
。
等値比較 - == /=
5 == 5 -- True
5 /= 5 -- False
!=
ではなく/=
。数学の≠をイメージしたものらしい。
関数適用
min 9 10 -- 9
max 9 10 -- 10
関数適用は
(スペース)
殆どの関数は前置記法で、演算子などの一部が中置記法。
10 `div` 2 -- 5
(+) 5 2 -- 7
``で囲むことで中置記法として記述できる。
逆に中置記法の演算子(正確には関数)を()で囲むことで前置記法ができる。
max 2 1 + 3 -- 5
(max 2 1) + 3
max 2 (1 + 3) -- 4
関数適用の結合優先度は最強。
順番を明示したい場合には、他の言語と同様に()で囲む。
関数定義(基本)
doubleMe x = x + x
doubleMe' x = x * 2
基本形は関数名 引数 = 本体
。
Haskellの慣習的に、違ったバージョン(遅延しないとか)の時は末尾に'
をつけたりする。
(数学でいうところのx
とx'
みたいな)
if-then-else
isZero x = if x == 0
then "Yes"
else "No"
ifは式なので値を返す。
そのためelse
は省略できない。
リスト
let lostNumbers = [4, 8, 15, 16, 23, 42]
[]
で囲って,
で区切って記述する。
[1, "Hello"] -- Compile Error
リスト内の要素はすべて同じ型である必要がある。
連結
[1, 2, 3] ++ [4, 5, 6] -- [1,2,3,4,5,6]
++
でリスト同士を連結。
連結するときに1個目のリストの最後まで走査するのでパフォーマンスは(1個目のリストの要素数に対して)O(n)。
先頭への要素追加
0 : [1, 2, 3] -- [0,1,2,3]
リストの先頭に要素を追加するときは:
(cons演算子)を使う。
Haskellのリストはconsリストなので、先頭への要素追加は一瞬で終わる。
ちなみに「cons」は「construction(構築)」の略らしい。
シンタックスシュガー
0 : 1 : 2 : 3 : [] -- [0,1,2,3]
前述してきたリスト表記は上記のシンタックスシュガー。
[]
は空のリスト。
いわゆる単方向連結リストのイメージでOK。
[0] --> [1] --> [2] --> [3] --> []
末尾の[]
は他の言語ではnull
やnil
が格納されることが多い(印象)。
要素へのアクセス
"Hello" !! 1 -- 'e'
"Hello" !! 5 -- *** Exception: Prelude.!!: index too large
!! index
でアクセス。
範囲外にアクセスするとエラーになる。(ちなみにLisp派生のClojureだとnil
を返す)
リスト in リスト
[[1, 2, 3], [4, 5, 6]]
リストの要素にリストを含めてもOK。
(リストの型が[Int]
が[[Int]]
になるようなもの)
リスト操作関数
head / tail / init / last
head [1, 2, 3, 4, 5] -- 1
tail [1, 2, 3, 4, 5] -- [2,3,4,5]
init [1, 2, 3, 4, 5] -- [1,2,3,4]
last [1, 2, 3, 4, 5] -- 5
リスト要素(範囲)を取り出す。
head [] -- *** Exception: Prelude.head: empty list
いずれも空のリストに対して適用するとエラーになるので注意。
length
length [1, 2, 3, 4, 5] -- 5
リストの長さを取得する。
null
null [1, 2, 3]
null []
リストが空かどうか調べる。
reverse
reverse [1, 2, 3, 4, 5] -- [5,4,3,2,1]
リストを反転させる。
take / drop
take 3 [1, 2, 3, 4, 5] -- [1,2,3]
drop 3 [1, 2, 3, 4, 5] -- [4,5]
take
は先頭から指定された数の要素を「取り出した(take)」リストを返す。
drop
は先頭から指定された数の要素を「除外した(drop)」リストを返す。
take 10 [1, 2, 3, 4, 5] -- [1,2,3,4,5]
drop 10 [1, 2, 3, 4, 5] -- []
それぞれリストの要素数を超えても機能する。
maximum / minimum
maximum [1, 2, 3, 4, 5] -- 5
minimum [1, 2, 3, 4, 5] -- 1
最大値、最小値をそれぞれ取り出す。
ただし、リストの要素は「順序付け」されている必要がある。
sum / product
sum [1, 2, 3, 4, 5] -- 15
product [1, 2, 3, 4, 5] -- 120
和、積、をそれぞれ返す。
elem
4 `elem` [1, 2, 3, 4, 5] -- True
6 `elem` [1, 2, 3, 4, 5] -- False
リストに指定した要素が含まれているか判定する。
(他の言語ではcontains
という名前がよく使われる印象)
elem 4 [1, 2, 3, 4, 5] -- True
もちろん前置記法で書いてもOK。
(慣れてくるとこっちのほうが関数っぽくて読みやすく感じたりする)
レンジ(Range)
[1..10] -- [1,2,3,4,5,6,7,8,9,10]
[2, 4..20] -- [2,4,6,8,10,12,14,16,18,20]
[開始値..終了値]
でレンジを指定することができ、その範囲のリストが生成できる。
[開始値, 次の値..終了値]
でステップ(増分値)を指定したレンジになる。
[10, 9..1] -- [10,9,8,7,6,5,4,3,2,1]
値が減少していくリストを作る場合は[開始値, 次の値..終了値]
という指定にする必要がある。
(例えば[10..1]
とやると、空のリスト[]
が結果として得られてしまう)
無限リスト
[1..] -- [1, 2, 3, ...]
Haskellは遅延評価なので、無限リストを作ることが可能。
上記をREPLなどで評価すると止まらない。(計算機のリソースが尽きるまで)
take 5 [1..] -- [1,2,3,4,5]
take
などを使って、(無限)リストの一部だけを取り出せる。
repeat / cycle / replicate
take 5 (repeat 3) -- [3,3,3,3,3]
take 5 (cycle [1, 2, 3]) -- [1,2,3,1,2]
replicate 3 10 -- [10,10,10]
repeat
は指定された値、cycle
は指定されたリスト、を繰り返す無限リストを生成。
replicate
は指定された長さと要素で(無限ではない)リストを作成する。
リスト内包表記
基本
[ x * 2 | x <- [1..10] ] -- [2,4,6,8,10,12,14,16,18,20]
リストの「フィルタリング」「変換」「組み合わせ」を行う方法の1つ。
[ 出力値 | 束縛変数 <- リスト ]
という形が基本形で、これだけであればmap
関数に近い。
フィルタ
[ x * 2 | x <- [1..10], x > 5 ] -- [12,14,16,18,20]
,
の後に述語を指定することでフィルタできる。
([ 出力値 | 束縛変数 <- リスト, フィルタ ]
)
組み合わせ
[ x + y | x <- [1, 2, 3], y <- [10, 100] ] -- [11,101,12,102,13,103]
2つのリストの要素を束縛して処理した場合、全組み合わせが計算される。
([ 出力値 | 束縛変数1 <- リスト1, 束縛変数2 <- リスト2 ]
)
[ x + y | x <- [1, 2, 3], y <- [10, 100], (x + y) > 100 ] -- [101,102,103]
前述したフィルタも一緒に使える。
([ 出力値 | 束縛変数1 <- リスト1, 束縛変数2 <- リスト2, フィルタ ]
)
ちなみにフィルタのことを「述語」と呼んだりする。(他の言語でも)
タプル
基本
(1, 2)
(1, 'A', "Foo")
値の組を表現するときに使う。
リストと違って異なる型でもOK。
2つ以上の組み合わせで、1つの値を表現したい時に使うイメージ。
fst / snd
fst (1, 'A') -- 1
snd (1, 'A') -- 'A'
要素数が2つのタプル(ペア)限定で、fst
で1つ目、snd
で2つ目、の要素が取得できる。
zip
zip [1, 2, 3] ['A', 'B', 'C'] -- [(1,'A'),(2,'B'),(3,'C')]
2つのリストを受け取って、それをタプル(ペア)のリストに結合する。
カバンとかジャケットとかの「ジッパー」を「閉める」ときの様子をイメージすると直感的。
(JOJO好きには「スティッキー・フィンガーズ」の逆バージョンと言えばOKかも)
文字列
"Hello"
'H' : 'e' : 'l' : 'l' : 'o' : []
文字列は""
、文字は''
で囲む。
Haskellにおいて「文字列」は「文字のリスト」なので、上記のコードは等価。