Haskell (と関数型プログラミング)を学ぶ
関数型プログラミングを学びたいと思っていたところ、三宮のジュンク堂書店で「すごいHaskellたのしく学ぼう!」を見つけたので勢いで購入した。
学習前の私は、
- カリー化って引数の数を減らすやつだよな?
- モナドってなんや、ゼノブレイドで出てきたやつとは違うんか
- 参照透過性は聞いたことがある
という状態だった。
環境構築
「すごいHaskellたのしく学ぼう!」には Haskell Platform を使って環境構築するように書かれているが、2024年3月27日現在で Haskell Platform は deprecated になっている。
そのため、Haskell 公式サイトに従って GHCup をインストールした。
以下のソフトウェアがインストールされたはず。
- GHC v9.4.7 (コンパイラ)
- Cabal v3.10.2.1 (ビルドツール)
- Stack v2.13.1 (ビルドツール)
- HLS v2.7.0.0 (Language Server)
Stack は Cabal の後発のビルドツールらしく、とりあえずどちらもインストールしておくと困ることが無さそうとのこと[1]。
あと VSCode の Haskell 用拡張機能もインストールした。
-
https://www.haskell.org/get-started/ 2024/03/27閲覧 ↩︎
1章 はじめの第一歩
これまで学んだ Go や TypeScript といったプログラミング言語とは大きく異なる。
- 関数呼び出しは
()
を使わない。ほとんどの関数で、引数は関数名の後ろにスペース区切りで書く。succ 1
など。 -
+
や*
といった算術演算子でさえ関数。これらは中置関数と呼ばれる。 -
if
は文ではなく式。必ず値を返す必要があるのでif
式にはelse
が必須。 - 引数を取らない関数は名前や定義と呼ばれ、immutable である。
- 強力なリスト内包表記が存在する。
[ x * 2 | x <- [1..20], odd x ]
で 1 から 20 までの数のうち、偶数を 2 倍にしたリスト[2,6,10,14,18,22,26,30,34,38]
が返ってくる。- このとき、
x
は<-
によって[1..20]
の各要素に束縛されるという。
- このとき、
- tuple は複数の型が共存できる。また、データの数と tuple を構成するデータの型によってその tuple の型が決定される。例えば
(1, 2, 3)
と(1, 2, "hello")
は別の型である。
関数ですべてを構成していくという思想が見えた気がする。
この本は GHC v6.12.3 の出力結果を書いているようだが、現在私のローカル PC にインストールされている GHC v9.4.7 とはエラーメッセージ周りがかなり異なるようだ。
2章 型を信じろ!
型クラスという概念が登場した。
- 型クラスは型が実装するべきインターフェースである。
- 型が持つべき振る舞いを抜き出したものと言えるのだろうか。
-
sum :: (Foldable t, Num a) => t a -> a
のように、型制約を書くことができる。-
Foldable
やNum
は型クラスである。
-
- 型クラスは OOP のクラスとは異なる概念である。
また、Haskell では多相的関数 (ジェネリクスのようなもの) がかなり簡単に書ける。例えば、
ghci> myFunc a = head a
ghci> :t myFunc
myFunc :: [a] -> a
のような形である。
型クラスは自作しないとあんまり理解できない気がした。
3章 関数の構文
パターンマッチ
束縛を行う構文では、パターンマッチが利用可能である。
-- 関数の引数に対するパターンマッチ
greeting :: String -> String
greeting "John" = "Hello, John!"
greeting "Emily" = "Hello, Emily!"
greeting name = "Nice to meet you, " ++ name ++ "!"
-- 実は上記は case 式の syntax sugar である。
greeting' :: String -> String
greeting' name = case name of
"John" -> "Hello, John!"
"Emily" -> "Hello, Emily!"
name -> "Nice to meet you, " ++ name ++ "!"
-- 構造に対するパターンマッチ
xs :: [Int]
xs = [1, 2, 3]
(x:y:z:[]) = xs -- x: 1, y: 2, z: [3]
[x', y', z'] = xs -- x': 1, y': 2, z': 3
(a:_) = xs -- a: 1
ガード
コードを上から走査し、パイプ文字の後の bool 値が True
と評価されたところの後の部分の関数本体が実行される。where
節を使ってガード内でのみ参照できる変数を定義することができる。
otherwise
は True
である[1]。
myGreet :: String -> String
myGreet name
| name == "yamada" = greet ++ "yamada"
| name == "tanaka" = greet ++ "tanaka"
| otherwise = "Nice to meet you"
where
greet = "Hello"
let 式
let
式は let binding in expression
の形で使用できる式 (expression
を評価した値を返す) である。
myGreet' :: String -> String
myGreet' name =
let greet = "Hello"; niceToMeetYou = "Nice to meet you"
in case name of
"yamada" -> greet ++ "yamada"
"tanaka" -> greet ++ "tanaka"
x -> niceToMeetYou ++ "x"
4章 Hello 再帰!
再帰関数に慣れるための章。再帰関数そのものは Haskell 特有のものではないが、手続き的なプログラミングに脳を支配されていたので頭の体操になった。
計算結果が何であるかを宣言的 (?) に記述するために、再帰がよく用いられるらしい。
例 配列の最大値を求めるプログラム
maximum' :: (Ord a) => [a] -> a
maximum' [] = error "list is empty"
maximum' [x] = x
maximum' (x : xs) = max x (maximum' xs)
Python でも同じようなものを書くことができる。
def maximum[T](l: list[T]) -> int:
if len(l) == 0:
raise ValueError("list is empty")
if len(l) == 1:
return l[0]
return max(l[0], maximum(l[1:]))
例 クイックソート
quickSort' :: (Ord a) => [a] -> [a]
quickSort' [] = []
quickSort' (x : xs) = sort' [y | y <- xs, y <= x] ++ [x] ++ sort' [y | y <- xs, y > x]
Python でも同じようなものを書くことができる。
def quick_sort[T](l: list[T]) -> list[T]:
if len(l) == 0 or len(l) == 1:
return l
return quick_sort([item for item in l[1:] if item <= l[0]]) \
+ [l[0]] + quick_sort([item for item in l[1:] if item > l[0]])
再帰は美しいですね。