すごいHaskellたのしく学ぼう①:概要と基本構文
すごいHaskellたのしく学ぼう! 1章のまとめ
英語版はネットにあって無料で読めます:)
私のメイン言語はKotlinでDDD(関数型DDD)で開発してるのですが、
関数型言語について,DDDについて理解を深めるべく
Haskellを学び始めたのでまとめ第一弾です。
これから書いていく内容は、上記の本を読みながら、
以下の資料も元に作成していますのでこちら理解のために参考になるかと思います。
イントロダクション
📚関数型言語Haskellとは?基本的な特徴概要
Haskellは純粋関数型言語であり、以下の特徴を持っている。
- 純粋関数型言語
-
静的型付け言語であり型推論をもつ:
コンパイル時に型チェック,明示的な型宣言がなくても良い。 - 不変性: 変数は一度定義されると変更できない(変数の緊縛/binding)
- 副作用を持たない: 関数は入力から出力を計算するだけ
- 参照透明性: 同じ入力に対して常に同じ結果を返す
- 遅延評価: 結果が必要になるまで計算を行わない
関数型言語と命令型言語の違い
項目 | 関数型 | 命令型 |
---|---|---|
設計思想 | 「何をすべきか」を記述 | 「どうやるか」を指示 |
状態管理 | 不変(immutable) | 可変(mutable) |
副作用 | 基本的になし | 自由に行える |
関数の定義 | 数学における計算を行うもの | 手続きをまとめたもの |
制御構造 | 関数の組み合わせ | ループや条件分岐 |
代表例 | Haskell, Scala, F# | C, Java, Python |
これらについて、ここから詳しく見ていく。
初めの第1歩
📚Haskellの基本:GHCとは
GHC(Glasgow Haskell Compiler)は最も広く使用されている
Haskellのオープンソースコンパイラ。
- 対話的インタプリタ(GHCi): コードを対話的に試せる環境
- 一括処理式コンパイラ(GHC): コードをコンパイルして実行ファイルを生成
1-1. 関数呼び出し: Haskellプログラミングの基礎について
Haskellでは2種類の関数呼び出し形式がある。
形式 | 説明 | 例 |
---|---|---|
前置関数 | 関数名が引数の前に来る |
succ 8 (結果は9) |
中置関数 | 2つの引数の間に関数名を置く |
8 * 5 (結果は40) |
-
関数適用の優先順位について:
関数適用が他のすべての演算(この場合は+
演算)よりも高い優先順位を持つ
ex.
ghci> succ 9 + max 5 4 + 1
16
ghci> ( succ 9 ) + ( max 5 4 ) + 1
16
「関数が優先」= succ関数とmax関数が優先して計算されるため、
succ 9
= 10, max 5 4
= 5,が先に計算されるため、答えが16になる。
- 関数を中置関数として扱いたいときはバッククォート``を使用すること。
Haskellの関数は通常前置記法で使用される。
1-2. 赤ちゃんの最初の関数
関数を作って読み込み実行する
-
.hs
フィルを作成、関数を書いてみる
doubleMe x = x + x
- そのファイルを作成したディレクトリでghciを起動
- 作成ファイルを読み込むには以下のコマンドを実施
ghci> :l ファイル名
<!-- もしくは以下のように -->
ghci> :load ファイル名
- 実行してみる
ghci> doubleMe 9
18
ghci> doubleMe 8.3
16.6
編集し再度読み込む
今度はさっき作ったファイルに他の関数も作成する
<!-- さっき書いた関数 -->
doubleMe x = x + x
<!-- 二つの引数をそれぞれ2倍してから足し合わせる関数 -->
doubleUs x y = x * 2 + y * 2
ghci> doubleUs 4 9
26
ghci> doubleUs 28 88 + doubleMe 123
478
▶︎ 組み合わせる
さっきの関数を少々変えてみる。
doubleUs x y = x * 2 + y * 2
の x * 2 は、先に定義したdoubleMeのことだ。よって以下のようにできる
doubleMe x = x + x
doubleUs x y = doubleMe x + y * 2
Haskellのif式: 式としてのif | 命令型言語との違い
Haskellのifは文ではなく式。
- 必ず値を返す必要がある
- else節が必須
- if式全体が一つの式として評価される
doubleSmallNumber x = if x > 100
then x
else x * 2
補足:式と文
特徴 | 式(Expression) | 文(Statement) |
---|---|---|
値の生成 | 常に値を返す | 必ずしも値を返さない |
組み合わせ | 他の式の一部として使用可能 | 通常、他の文に直接埋め込めない |
shellで直打ちするなら以下のように囲って書くこと。
ghci> doubleSmallNumber' x = (if x > 100 then x else x * 2)
'
(アポストロフィ) について
🌱 Haskell における- Haskellでは関数名にアポストロフィ(
'
)を使うことができ、
慣習的に正格(遅延じゃない)版の関数を表したり、
少し変更した場合に元の関数名に似た名前にするために使割れることが多い
(特別な構文上の意味は持たない)
📖 Haskell における '(シングルクォート)と "(ダブルクォート)
🌱 'A'(シングルクォート)は Char 型
- 'A' のようにシングルクォートで囲まれたものは、1つの文字 (Char 型) を表す。
- 1文字のみ を扱う場合に使用。
🌱 "A"(ダブルクォート)は String 型
- "A" のようにダブルクォートで囲まれたものは、文字列 (String 型) を表す。
- Haskell において String は [Char](Char のリスト) として実装されている。
- 複数の文字を扱う場合 に使用。
1-3. HaskellのList入門
Listについて書く前に....
- GHCIの中で名前定義は
let
を使用すること- 定義した変数は不変だよ
- Listは一様なデータ構造であること(= 要素が同一データ型であること)
ghci> let lostNumber = [4,8,15,16,23,42]
ghci> lostNumber
[4,8,15,16,23,42]
いろんなリスト操作
操作 | 演算子/関数 | 説明 | 例 |
---|---|---|---|
連結 | ++ |
2つのリストを連結 |
[1,2] ++ [3,4] → [1,2,3,4]
|
先頭追加 | : |
要素をリストの先頭に追加 |
5:[1,2,3] → [5,1,2,3]
|
インデックスアクセス | !! |
指定位置の要素を取得 |
[1,2,3] !! 1 → 2
|
Haskellのリスト操作関数
🍃 基本的なリスト操作関数
関数 | 説明 | 例 | 空リスト時の挙動 |
---|---|---|---|
head |
リストの最初の要素を返す |
head [1,2,3] → 1
|
エラー *** Exception: Prelude.head: empty list
|
tail |
最初の要素を除いたリストを返す |
tail [1,2,3] → [2,3]
|
エラー *** Exception: Prelude.tail: empty list
|
last |
リストの最後の要素を返す |
last [1,2,3] → 3
|
エラー *** Exception: Prelude.last: empty list
|
init |
最後の要素を除いたリストを返す |
init [1,2,3] → [1,2]
|
エラー *** Exception: Prelude.init: empty list
|
length |
リストの長さを返す |
length [1,2,3] → 3
|
安全:length [] → 0
|
null |
リストが空かどうかを判定 |
null [1,2,3] → False
|
安全:null [] → True
|
reverse |
リストを逆順にする |
reverse [1,2,3] → [3,2,1]
|
安全:reverse [] → []
|
take |
先頭からn個の要素を取る |
take 2 [1,2,3] → [1,2]
|
安全:take n [] → []
|
drop |
先頭からn個の要素を除いたリストを返す |
drop 2 [1,2,3] → [3]
|
安全:drop n [] → []
|
maximum |
リスト内の最大値を返す |
maximum [1,5,3] → 5
|
エラー *** Exception: Prelude.maximum: empty list
|
sum |
リスト内の要素の合計を返す |
sum [1,2,3] → 6
|
安全:sum [] → 0
|
elem |
要素がリストに含まれるか判定 |
elem 2 [1,2,3] → True
|
安全:elem x [] → False
|
💡 安全なリスト操作のパターン
空リストによるエラーを避けるために、以下のパターンを使用することができる。
- パターンマッチングを使用する
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
safeTail :: [a] -> [a]
safeTail [] = []
safeTail (_:xs) = xs
-
null
でリストが空かどうかを事前に確認する
getFirstElement xs = if null xs then error "Empty list" else head xs
-
Data.Maybe
モジュールの関数を利用する
import Data.Maybe (listToMaybe)
-- listToMaybeは安全にheadの代わりになる
safeHead' = listToMaybe -- safeHead' [1,2,3] → Just 1, safeHead' [] → Nothing
📚 実践的な例
-- リストが空でないことを確認してから操作
processNonEmptyList :: [Int] -> Int
processNonEmptyList xs
| null xs = 0 -- 空リストの場合は0を返す
| otherwise = head xs + last xs -- 最初と最後の要素の合計
-- Maybeを使った安全な操作
safeLast :: [a] -> Maybe a
safeLast [] = Nothing
safeLast xs = Just (last xs)
-- 複数の関数を組み合わせる
takeReverse :: Int -> [a] -> [a]
takeReverse n = reverse . take n -- n個取って逆順にする
1-4. レンジでチン!
リスト範囲表記(Range)
連続した数値やその他の列挙可能な値のリストを簡単に生成するための範囲表記(Range)
🌱基本的な範囲表記
[start..end]
という構文で連続した値のリストを生成可能。
-- [1,2,3,4,5] 開始値から終了値までの連続した値
ghci> [1..5]
[1,2,3,4,5]
-- "abcde" 文字も範囲表記が使える
ghci> ['a'..'e']
"abcde"
- ⚠️ 減少列を作りたい時は、注意!
-- 不正(空リスト!自動的に降順にはならない)
ghci> [5..1]
[]
-- 正しい。
ghci> [5,4..1]
[5,4,3,2,1]
🌱ステップ(増分)の指定
最初の2つの要素をカンマで区切って記述し、その後に上限値を指定
[1,3..10] -- [1,3,5,7,9] 2つごとに増加(増分は2)
[2,4..10] -- [2,4,6,8,10] 2つごとに増加
[5,10..25] -- [5,10,15,20,25] 5つごとに増加
[10,9..1] -- [10,9,8,7,6,5,4,3,2,1] 減少する範囲
🌱無限リスト
上限を省略すると無限リストになります
[1..] -- 1から始まる無限リスト [1,2,3,...]
[2,4..] -- 2から始まる偶数の無限リスト [2,4,6,...]
Haskellは遅延評価(lazy evaluation)を採用しているため、
無限リストを扱うことができる。
無限リストは必要な部分だけが評価されるため、take
や!!
などの関数と組み合わせて使用します。
(Ex. )
-- 最初の24個の13の倍数を取得する
take 24 [13,26..] -- 13から始まる13の倍数の無限リストから24個取得
-- 以下も同じ意味
[13,26..24*13] -- 上限を計算して範囲指定
他、便利な無限リスト生成関数
-
cycle
- リストを無限に繰り返すtake 10 (cycle [1,2,3]) -- [1,2,3,1,2,3,1,2,3,1] take 12 (cycle "LOL ") -- "LOL LOL LOL "
-
repeat
- 要素を無限に繰り返すtake 10 (repeat 5) -- [5,5,5,5,5,5,5,5,5,5]
-
replicate
- 要素を指定回数繰り返す(repeat
とtake
の組み合わせと同等)replicate 10 5 -- [5,5,5,5,5,5,5,5,5,5] replicate 3 "hello" -- ["hello","hello","hello"]
関連する便利な関数
範囲表記は内部的に以下の関数に変換される
-
enumFrom x
:[x..]
と同等enumFrom 1 -- [1,2,3,4,5...](無限リスト) enumFrom 'a' -- "abcdef..."(無限リスト)
-
enumFromTo x y
:[x..y]
と同等enumFromTo 1 10 -- [1,2,3,4,5,6,7,8,9,10] enumFromTo 'a' 'f' -- "abcdef"
-
enumFromThen x y
:[x,y..]
と同等(xとyの差がステップになる)enumFromThen 1 3 -- [1,3,5,7,9...](無限リスト、ステップ2) enumFromThen 5 2 -- [5,2,-1,-4...](無限リスト、ステップ-3)
-
enumFromThenTo x y z
:[x,y..z]
と同等enumFromThenTo 1 3 10 -- [1,3,5,7,9] enumFromThenTo 10 8 1 -- [10,8,6,4,2]
これらの関数はEnum
型クラスのメソッドだから、
Int
、Char
、Float
などEnum
のインスタンスとなる型で使用可能。
🌱 浮動小数点数との使用
⚠️ 丸め誤差に注意が必要
[0.1, 0.3 .. 1.0] -- [0.1,0.3,0.5,0.7,0.9]
1-5. リスト内包表記
数学の集合表記(集合の内包的記法)に似た記法で、
リストのフィルタリング、変換、組み合わせを行う方法。
🌱基本構文
「|」(縦線)の
左側に出力する式
を、
右側に条件(ジェネレーター(要素 <- リスト)
)を記述する。
[式 | 要素 <- リスト, 条件1, 条件2, ...]
👀 Ex.
-- 1から10までの数の2倍のリスト
ghci> [x * 2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]
-- 1から10までの偶数のみの2倍のリスト
ghci> [x * 2 | x <- [1..10], even x]
[4,8,12,16,20]
-- 条件を複数指定(2と3の両方で割り切れる数)
ghci> [x | x <- [1..20], x `mod` 2 == 0, x `mod` 3 == 0]
[6,12,18]
1-6.タプル
- 異なる型の要素を固定長のグループとして扱うためのデータ構造
- リストとは異なり、異なる型の値を格納でき(=ヘテロ)、長さは不変
- 空のタプル () は"ユニット"と呼ばれ、値を持たないことを示す(他の言語の void に相当)
🌱 基本構文
-- 2要素のタプル(ペア)
pair :: (Int, String)
pair = (1, "apple")
-- 3要素のタプル(トリプル)
triple :: (Int, String, Bool)
triple = (2, "banana", True)
🌱タプルへのアクセス方法
- パターンマッチング
-- タプルの分解
(a, b) = (1, "apple") -- a = 1, b = "apple"
-- 関数の引数でのパターンマッチング
sumPair :: (Int, Int) -> Int
sumPair (x, y) = x + y
- アクセス関数(ペア専用)
fst (1, "apple") -- 1 (1番目の要素を取得)
snd (1, "apple") -- "apple" (2番目の要素を取得)
🌱 ペアを使う: タプル操作
Haskellではペア(タプル)を使って値を格納するための関数について。
以下の関数はペアにのみ適用でき、トリプルや4-タプル、5-タプルなどには使えない
関数 | 説明 | 例 |
---|---|---|
fst |
ペアの1つ目の要素を返す |
fst (8, 11) → 8
|
snd |
ペアの2つ目の要素を返す |
snd (8, 11) → 11
|
zip |
2つのリストからペアのリストを作る |
zip [1,2] ["a","b"] → [(1,"a"),(2,"b")]
|
🌱zip関数について
2つのリストを受け取り、ペアのリストを作る方法。
リストを並行処理するパターンでよく使われる。2つのリストを同時に走査するのに便利。
リスト長が異なる場合、短い方に合わせる(余りは無視される)
→ 遅延評価のため、無限リストと有限リストのzipも可能
ghci> zip [1,2,3,4,5] [5,5,5,5,5]
[(1,5),(2,5),(3,5),(4,5),(5,5)]
ghci> zip [1..5] ["one", "two", "three", "four", "five"]
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]
ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]
[(5,"im"),(3,"a"),(2,"turtle")]
Discussion