Haskell
目次
主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。
- 概要
- 1. Haskellとは何か・なぜ生まれたか
- 2. 環境構築(GHC / Cabal / Stack / GHCup)
- 3. 純粋関数型とは何か
- 4. 型システムの基礎
- 5. 関数定義とパターンマッチ
- 6. 高階関数とラムダ
- 7. リストと内包表記
- 8. タプル
- 9. 代数的データ型(ADT)
- 10. 型クラス
- 11. 遅延評価
- 12. 入出力とIOモナド
- 13. モナドとdo記法
- 14. ファンクター・アプリカティブ・モナド
- 15. ジェネリクスとparametric polymorphism
- 16. 型クラスと制約
- 17. 例外とエラー処理
- 18. レコード構文
- 19. モジュールとパッケージ
- 20. 並行・並列処理
- 21. STM(Software Transactional Memory)
- 22. パフォーマンスとプロファイル
- 23. テスト戦略
- 24. GHC拡張
- 25. 主要ライブラリ
- 26. よくある落とし穴FAQ
- 27. 実践パターン集
- 28. 学習ロードマップ(30日)
- 29. 用語集
- 発展: 型と関数型設計
- 応用: 抽象化とプロジェクト設計
- 実践: ランタイムとコミュニティ
- 参照: 型クラスと実例
- 運用: ツールとプロダクション
- 補遺: 実例とFAQ
- 上級: 型レベルとEffect
- 上級: コンパイラと最適化
- 補遺: Haskellの設計思想
- Haskell エコシステム と 学習リソース
- Haskell による関数型プログラミングの実践
- Haskell パフォーマンスチューニング
- Haskell によるWeb開発
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
Haskellは純粋関数型・遅延評価・強力な静的型システムを持つ汎用プログラミング言語です。1990年に学術コミュニティで設計され、関数型プログラミングの研究言語として進化しつつ、産業利用も増えています。型クラス、モナド、ADT、型推論などの概念は他言語に大きな影響を与えました。
このページでは、純粋性、遅延評価、型クラス、モナド、ADT、IO、並行性を中心に、Haskellの設計思想とともに整理します。
1. Haskellとは何か・なぜ生まれたか
このセクションでは「Haskellがなぜ生まれたのか」「純粋関数型とは何か」「なぜ業務で使う価値があるのか」を整理します。
Haskellは 「純粋関数型・遅延評価・静的型」 の三本柱を持つ言語。
Haskell = 純粋関数型 + 遅延評価 + 型クラス + 強力な型推論
1-1. 関数型プログラミングの歴史
1958 Lisp(最初の関数型)
1970年代ML、Scheme
1985 Miranda(純粋関数型・遅延評価の元祖)
1987研究者たちが集まり、純粋関数型言語の標準化を議論
1990 Haskell 1.0
1998 Haskell 98(標準化)
2010 Haskell 2010(小幅な改訂)
継続GHC拡張で実質的に進化
「Haskell」は数学者Haskell Curryにちなんで命名。**「カリー化(currying)」**の元になった人。
1-2. なぜHaskellか
研究者たちは 「純粋関数型・遅延評価」を実証する標準言語が欲しかった。Mirandaは商用ライセンスで研究に使いにくく、「研究と実用の橋渡しになる純粋関数型言語」としてHaskellが生まれました。
設計目標
- 純粋関数型: 副作用を型で表現
- 遅延評価: 必要になるまで計算しない
- 強い静的型: 型推論 + 型クラス
- 数学的厳密さ: ラムダ計算ベース
1-3. 純粋関数型の利点
- 同じ入力 → 同じ出力(参照透明性)
- 副作用がない(数学関数のように)
- 並行処理が安全(共有可変状態がない)
- テスト容易(純関数はモック不要)
- 等式推論(コードを式として書き換えられる)
「書きにくいが、書けたら正しい」と言われる所以。バグの少ないコードが手に入ります。
1-4. Haskellが動く場所
産業:
FacebookのSigma(スパム検出)
Standard Chartered(金融バックエンド)
GitHubのSemantic(コード解析)
Mercury(Haskellコンパイラ製金融サービス)
IOG(Cardano暗号通貨)
ツール:
Pandoc(マークアップ変換)
Shellcheck(Bashリンター)
Hasura(GraphQLバックエンド)
PostgREST(PostgreSQL → REST)
研究:
プログラミング言語論
形式手法
証明支援
ICFP / FPコミュニティ
「メインストリームではないが、重要なシーンで活躍」する独自のポジション。
1-5. このセクションのまとめ
- 1990年 学術コミュニティで設計
- 純粋関数型 + 遅延評価 + 型クラス
- 数学者Haskell Curryにちなむ
- Pandoc / Shellcheck / Cardanoなど実用例
- 「書ければ正しい」コード
2. 環境構築(GHC / Cabal / Stack / GHCup)
2-1. GHC(Glasgow Haskell Compiler)
GHCが事実上の標準コンパイラ
- 高品質なネイティブコード生成
- 並行・並列実行サポート
- 多数の言語拡張
- GHCi(REPL)
2-2. GHCup(推奨インストーラ)
# Linux/macOS
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
# 確認
ghcup tui # 対話的にGHC/Cabal/Stackを管理
ghc --version
cabal --version
stack --version
GHCupは GHC・Cabal・Stack・HLS(Haskell Language Server)を統一管理。Rubyのrbenvに相当。
2-3. Cabal vs Stack
Cabal: Haskell標準のビルドツール
Stack: パッケージ凍結とビルドの再現性に強み
最近はCabal v2-buildが改善されて、両者の差は縮まっています。新規はCabalでOK。
2-4. Cabalプロジェクト
cabal init -n -m # ライブラリ + 実行ファイル
cabal build
cabal run
cabal test
cabal repl # GHCi起動
-- my-app.cabal
cabal-version: 3.0
name: my-app
version: 0.1.0.0
library
exposed-modules: Lib
build-depends: base >= 4.14 && < 5,
text,
containers
hs-source-dirs: src
default-language: Haskell2010
executable my-app
main-is: Main.hs
build-depends: base, my-app
hs-source-dirs: app
2-5. Stackプロジェクト
stack new my-app
stack build
stack run
stack test
stack ghci
Stackは 「LTS Haskellスナップショット」を使い、再現性が高い。CI向き。
2-6. HLS(Haskell Language Server)
VSCode、Vim、Emacsに対応するLSPサーバ。型表示・補完・リファクタリングを提供。ghcup install hls で入る。
2-7. 簡単なREPL
$ ghci
GHCi, version 9.4.8: ...
ghci> 1 + 2
3
ghci> :type map
map :: (a -> b) -> [a] -> [b]
ghci> map (*2) [1,2,3]
[2,4,6]
ghci> :quit
:t(型表示)、:i(情報)、:l file.hs(ロード)、:r(再ロード)が頻出。
2-8. このセクションのまとめ
- GHCupでGHC + Cabal + Stack + HLSをまとめて管理
- Cabalが標準(Stackも健在)
- HLSでIDE体験
- ghciでREPL駆動
3. 純粋関数型とは何か
3-1. 純粋関数
「同じ入力なら必ず同じ出力」「副作用がない」関数。
-- 純粋
add :: Int -> Int -> Int
add x y = x + y
-- 不純(参照透明性なし)
-- fは同じ引数でも違う結果を返しうる
-- → Haskellでは型で副作用を表す
3-2. 参照透明性
「式をその値で置き換えても意味が変わらない」性質。
let x = 1 + 2
let y = x * x
-- 等価:
let y = 3 * 3
-- 等価:
let y = 9
これにより 等式推論ができ、コンパイラも積極的に最適化できる。
3-3. イミュータブルデフォルト
let x = 5
-- x = 6 -- これは新しい束縛、xは変わらない
すべての値が デフォルトで不変。可変変数は IORef などの特殊な型で扱う。
3-4. 副作用は型で表現
-- 副作用なし
square :: Int -> Int
square x = x * x
-- IO副作用がある
greet :: IO ()
greet = putStrLn "Hello"
-- IO Intは「IO副作用を含む計算で、最終的にIntを返す」
readNumber :: IO Int
readNumber = readLn
IO という型で 副作用が可能なことを明示。型を見るだけで「この関数は外部とやり取りするか」が分かる。これがHaskellの特徴的な設計。
3-5. このセクションのまとめ
- 純粋関数: 同じ入力→同じ出力、副作用なし
- 参照透明性で等式推論
- すべてイミュータブル
- 副作用はIO型で明示
- バグが減る、並行が安全
4. 型システムの基礎
4-1. 基本型
True, False :: Bool
'a' :: Char
"hello" :: String
42 :: Int -- 機械語整数
42 :: Integer -- 任意精度
3.14 :: Double
() :: () -- unit型
[1, 2, 3] :: [Int] -- リスト
(1, "a") :: (Int, String) -- タプル
4-2. 型推論
ghci> :t map (*2)
map (*2) :: Num a => [a] -> [a]
GHCは 強力な型推論を持ち、型注釈を書かなくても型が決まります。型注釈は ドキュメントとしては書く価値あり。
4-3. 関数の型
add :: Int -> Int -> Int
add x y = x + y
-- 部分適用(カリー化)
addOne :: Int -> Int
addOne = add 1
ghci> addOne 5
6
Int -> Int -> Int は実は Int -> (Int -> Int)。すべての関数は 1引数関数。これが カリー化。
4-4. 関数合成
ghci> (negate . abs) (-5)
-5
-- 等価
ghci> negate (abs (-5))
-5
-- $ で括弧を減らす
ghci> negate $ abs $ (-5)
-5
. は関数合成、$ は 「右側を1まとまり」にする。括弧を減らすために多用。
4-5. 多相型(型変数)
identity :: a -> a
identity x = x
length' :: [a] -> Int
length' [] = 0
length' (x:xs) = 1 + length' xs
a は型変数(小文字)。「任意の型」を意味する。Java/C# のジェネリクスに相当。
4-6. 型クラス制約
add :: Num a => a -> a -> a
add x y = x + y
ghci> :t (==)
(==) :: Eq a => a -> a -> Bool
Num a => は 「aはNum型クラスのインスタンス」という制約。Javaの <T extends Number> に相当。
4-7. このセクションのまとめ
- 強い静的型 + 型推論
- 関数はすべて1引数(カリー化)
- . で関数合成、$ で括弧削減
- 型変数(小文字)で多相
- => で型クラス制約
5. 関数定義とパターンマッチ
5-1. 基本
double :: Int -> Int
double x = x * 2
-- 複数引数
add :: Int -> Int -> Int
add x y = x + y
-- whereで局所定義
slope :: (Double, Double) -> (Double, Double) -> Double
slope (x1, y1) (x2, y2) = dy / dx
where
dy = y2 - y1
dx = x2 - x1
5-2. パターンマッチ
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
-- 上から順にマッチ、最初に成立したケースが選ばれる
5-3. リストのパターンマッチ
sumList :: [Int] -> Int
sumList [] = 0
sumList (x:xs) = x + sumList xs
-- (x:xs) は「先頭x、残りxs」
-- [a, b, c] はa:b:c:[]
head' :: [a] -> a
head' (x:_) = x
head' [] = error "empty"
-- _ はワイルドカード(値を捨てる)
5-4. ガード(条件付き)
classify :: Int -> String
classify x
| x > 0 = "positive"
| x < 0 = "negative"
| otherwise = "zero"
bmi :: Double -> Double -> String
bmi weight height
| b < 18.5 = "underweight"
| b < 25.0 = "normal"
| b < 30.0 = "overweight"
| otherwise = "obese"
where b = weight / height^2
| で条件分岐。otherwise は True と同じ。
5-5. let式
cylinder :: Double -> Double -> Double
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r^2
in sideArea + 2 * topArea
where は関数末尾、let は式の途中で局所束縛。
5-6. case式
describe :: Int -> String
describe x = case x of
0 -> "zero"
1 -> "one"
_ -> "many"
5-7. このセクションのまとめ
- 関数定義は = の連鎖
- パターンマッチで分岐
- ガード(| 条件 = ...)
- where(後置)/ let(前置)で局所束縛
- case式
6. 高階関数とラムダ
6-1. 関数を引数に
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
ghci> applyTwice (+3) 10
16
ghci> applyTwice (++ " HAHA") "HEY"
"HEY HAHA HAHA"
6-2. ラムダ
ghci> (\x -> x * 2) 5
10
ghci> (\x y -> x + y) 1 2
3
map (\x -> x * x) [1, 2, 3] -- [1, 4, 9]
\x -> ... がラムダ式。λ をモチーフにしているがASCII互換のため \ を使う。
6-3. 主要な高階関数
map :: (a -> b) -> [a] -> [b]
ghci> map (*2) [1,2,3]
[2,4,6]
filter :: (a -> Bool) -> [a] -> [a]
ghci> filter odd [1,2,3,4,5]
[1,3,5]
foldr :: (a -> b -> b) -> b -> [a] -> b
ghci> foldr (+) 0 [1,2,3]
6
foldl :: (b -> a -> b) -> b -> [a] -> b
ghci> foldl (+) 0 [1,2,3]
6
-- 推奨はfoldl' (strict foldl)、空間効率が良い
6-4. 関数合成とpointfree
-- ポイントフル
sumOfSquares xs = sum (map (^2) xs)
-- ポイントフリー(引数を書かない)
sumOfSquares = sum . map (^2)
「引数を書かないスタイル」。慣れると読みやすいが、過度なpointfreeは逆に読みにくい。
6-5. このセクションのまとめ
- 高階関数で関数を渡す/返す
- \x -> ... でラムダ
- map / filter / foldが定番
- foldl' で正格畳み込み
- 関数合成(.)とpointfreeスタイル
7. リストと内包表記
7-1. リスト基本
[1, 2, 3]
[1..10] -- [1,2,3,4,5,6,7,8,9,10]
[1,3..10] -- [1,3,5,7,9]
['a'..'z'] -- "abcdefghijklmnopqrstuvwxyz"
[1..] -- 無限リスト!
head [1,2,3] -- 1
tail [1,2,3] -- [2,3]
last [1,2,3] -- 3
init [1,2,3] -- [1,2]
length [1,2,3] -- 3
reverse [1,2,3] -- [3,2,1]
take 3 [1..] -- [1,2,3]
drop 2 [1..5] -- [3,4,5]
elem 3 [1,2,3] -- True
1:2:3:[] -- [1,2,3]
[1,2] ++ [3,4] -- [1,2,3,4]
7-2. リスト内包表記
[x*2 | x <- [1..5]] -- [2,4,6,8,10]
[x*2 | x <- [1..10], x*2 >= 12] -- [12,14,16,18,20]
[(x, y) | x <- [1..3], y <- [1..3], x /= y] -- ペア生成
-- ピタゴラス数
[(a,b,c) | c <- [1..20], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]
数学の集合内包と似た書き方。Pythonの内包表記の元ネタ。
7-3. 文字列はリスト
type String = [Char]
-- なので文字列にもリスト関数が使える
"hello" ++ " world" -- "hello world"
length "hello" -- 5
reverse "abc" -- "cba"
'A':"BC" -- "ABC"
ただし性能のため Data.Text が業務では推奨。
7-4. このセクションのまとめ
- [1..n] / [1,3..] / [1..] で範囲
- リスト内包表記 [expr | gen, cond, ...]
- 文字列 = [Char]
- 業務はText / ByteString
8. タプル
(1, "hello") -- (Int, String)
(1, 2, 3) -- 3要素タプル
fst (1, "a") -- 1
snd (1, "a") -- "a"
-- パターンマッチで分解
let (a, b) = (1, 2)
タプルは 要素数も型も固定。リストと違って 異なる型を混ぜられる。
-- zip
ghci> zip [1,2,3] ["a","b","c"]
[(1,"a"),(2,"b"),(3,"c")]
ghci> unzip [(1,'a'),(2,'b')]
([1,2],"ab")
9. 代数的データ型(ADT)
Haskellの 「型システムの中核」。
9-1. 列挙型
data Bool = True | False
data Color = Red | Green | Blue
describe :: Color -> String
describe Red = "red"
describe Green = "green"
describe Blue = "blue"
9-2. ペア型(直積)
data Point = Point Double Double
distance :: Point -> Point -> Double
distance (Point x1 y1) (Point x2 y2) =
sqrt ((x2-x1)^2 + (y2-y1)^2)
9-3. 直和型(タグ付き共用体)
data Shape
= Circle Double -- 半径
| Rectangle Double Double -- 幅、高さ
| Triangle Double Double Double -- 3辺
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
area (Triangle a b c) =
let s = (a + b + c) / 2
in sqrt (s * (s-a) * (s-b) * (s-c))
これが 「代数的データ型(ADT)」。Rustのenum、Swiftのenum、TypeScriptのdiscriminated unionに対応。
9-4. パラメータ付き型
data Maybe a = Nothing | Just a
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)
ghci> safeDiv 10 2
Just 5
ghci> safeDiv 10 0
Nothing
Maybe は nullの代わり。「あるかも、ないかも」を型で表現。Rustの Option、Swiftの Optional に対応。
9-5. Either
data Either a b = Left a | Right b
safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "Division by zero"
safeDiv x y = Right (x `div` y)
慣習的に Left = エラー、Right = 成功(Rightは “right” = 正しい)。Rustの Result に対応。
9-6. 再帰的型
data Tree a = Leaf | Node (Tree a) a (Tree a)
insert :: Ord a => a -> Tree a -> Tree a
insert x Leaf = Node Leaf x Leaf
insert x (Node left y right)
| x < y = Node (insert x left) y right
| x > y = Node left y (insert x right)
| otherwise = Node left y right
9-7. このセクションのまとめ
- data型名 = コンストラクタ | コンストラクタ | ...
- 列挙 / 直積 / 直和(タグ付き共用体)
- Maybe / Eitherでnull / エラーを型で
- 再帰的型でデータ構造
- パターンマッチで分解
10. 型クラス
「ad-hoc多相」を実現するHaskell独自の機能。
10-1. 標準的な型クラス
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
class Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>), (>=) :: a -> a -> Bool
class Show a where
show :: a -> String
class Read a where
read :: String -> a
10-2. インスタンス定義
data Color = Red | Green | Blue
instance Show Color where
show Red = "Red"
show Green = "Green"
show Blue = "Blue"
instance Eq Color where
Red == Red = True
Green == Green = True
Blue == Blue = True
_ == _ = False
10-3. deriving(自動導出)
data Color = Red | Green | Blue
deriving (Eq, Ord, Show, Read, Enum, Bounded)
ghci> show Red
"Red"
ghci> Red == Red
True
ghci> compare Red Green
LT
ghci> [Red ..]
[Red,Green,Blue]
deriving で 典型的な型クラスを自動実装。Rustの #[derive(Debug, Clone)] に相当。
10-4. 主要な型クラス
Eq: == / /=
Ord: compare、< > など
Show: show(文字列化)
Read: read(文字列から)
Enum: succ / pred / [a..b]
Bounded: minBound / maxBound
Num: +, -, *, fromInteger
Integral: div, mod, quot, rem
Fractional: /, recip
Floating: pi, exp, sqrt, sin
Functor: fmap
Applicative: pure, <*>
Monad: return, >>=
Foldable: foldr, foldl
Traversable: traverse
10-5. このセクションのまとめ
- type classでad-hoc多相
- instanceで型ごとの実装
- derivingで自動導出
- Eq / Ord / Show / Num / Functor / Monadが頻出
- Javaのinterfaceに近いが、より柔軟
11. 遅延評価
Haskellの最大の特徴のひとつ。「必要になるまで計算しない」。
11-1. 無限リスト
nats = [0..] -- 0, 1, 2, ... 無限
take 5 nats -- [0,1,2,3,4]
primes = sieve [2..]
where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
take 10 primes -- [2,3,5,7,11,13,17,19,23,29]
無限リストを定義しても、取り出す分だけ計算。
11-2. 短絡評価
firstNonZero :: [Int] -> Int
firstNonZero xs = head (filter (/= 0) xs)
ghci> firstNonZero [0, 0, 5, undefined]
5
-- undefinedは評価されない
11-3. メモリの落とし穴(thunk)
-- 大きなリストをfoldlで和を取る
sum' = foldl (+) 0 [1..1000000]
-- 内部的にthunk(未評価の式)が大量に積まれる、メモリ爆発
-- 解決: foldl' で正格畳み込み
import Data.List (foldl')
sum' = foldl' (+) 0 [1..1000000]
「遅延評価が悪さをする」のがHaskellの主要な落とし穴。foldl'、Data.Map.Strict、!(bang pattern)で正格化。
11-4. 正格性アノテーション
{-# LANGUAGE BangPatterns #-}
sumStrict :: [Int] -> Int
sumStrict = go 0
where go !acc [] = acc
go !acc (x:xs) = go (acc + x) xs
!acc で その場で評価。
11-5. このセクションのまとめ
- 必要になるまで計算しない
- 無限リストが書ける
- thunkの積み上がりに注意
- foldl'(正格)/ foldr(遅延)の使い分け
- !x(bang pattern)で正格化
12. 入出力とIOモナド
純粋関数型で副作用をどう扱うか ─ Haskellの答えが IO 型。
12-1. main関数
-- hello.hs
main :: IO ()
main = putStrLn "Hello, World!"
ghc hello.hs && ./hello
# Hello, World!
# または
runghc hello.hs
main :: IO () は 「IO副作用を持ち、最終的に値なし(()) を返す」。
12-2. 基本的なIO関数
putStrLn :: String -> IO () -- 文字列出力 + 改行
putStr :: String -> IO ()
print :: Show a => a -> IO ()
getLine :: IO String -- 1行読む
readLn :: Read a => IO a -- 1行読んでパース
getChar :: IO Char
12-3. do記法
greet :: IO ()
greet = do
putStrLn "What's your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
do で 複数のIOアクションを順次実行。<- でIO結果を取り出す。
12-4. IOは値
hello :: IO ()
hello = putStrLn "hello"
main :: IO ()
main = do
hello
hello
hello
hello は「putStrLnを実行する命令」を表す 値。実行は main で行われる。
12-5. ファイルI/O
main :: IO ()
main = do
contents <- readFile "input.txt"
let upper = map toUpper contents
writeFile "output.txt" upper
12-6. このセクションのまとめ
- IO型で副作用を表現
- main :: IO ()
- do記法で順次実行
- <- でIO結果を取り出す
- readFile / writeFile / putStrLnなど
13. モナドとdo記法
13-1. モナドとは
「計算の文脈を扱うインターフェース」。IO、Maybe、Either、List、State などすべてモナド。
class Monad m where
return :: a -> m a -- 単純な値を文脈に入れる
(>>=) :: m a -> (a -> m b) -> m b -- 連結
13-2. do記法はモナドの糖衣
-- これは
do x <- foo
y <- bar x
return (x + y)
-- これと等価
foo >>= \x ->
bar x >>= \y ->
return (x + y)
do 記法は 任意のモナドで使える。IOに限らない。
13-3. Maybeモナド
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)
calc :: Int -> Int -> Int -> Maybe Int
calc a b c = do
x <- safeDiv a b
y <- safeDiv x c
return (y + 1)
ghci> calc 10 2 5
Just 2
ghci> calc 10 0 5
Nothing -- 短絡
「途中でNothingが出たら全体がNothing」。nullチェックの連鎖を きれいに書ける。
13-4. Eitherモナド
parseNum :: String -> Either String Int
parseNum s = case reads s of
[(n, "")] -> Right n
_ -> Left ("Cannot parse: " ++ s)
calc :: String -> String -> Either String Int
calc s1 s2 = do
a <- parseNum s1
b <- parseNum s2
return (a + b)
エラー処理の連鎖。
13-5. Listモナド
pairs :: [(Int, Int)]
pairs = do
x <- [1, 2, 3]
y <- [10, 20]
return (x, y)
-- [(1,10),(1,20),(2,10),(2,20),(3,10),(3,20)]
リスト内包表記と等価。
13-6. このセクションのまとめ
- モナドは「文脈付き計算」のインターフェース
- IO / Maybe / Either / List / Stateなどモナド
- do記法でフラットに書ける
- Maybeでnull処理、Eitherでエラー処理
- 内包表記とListモナドは等価
14. ファンクター・アプリカティブ・モナド
Functor: fmapで値を変換
Applicative: 複数の文脈付き値を組み合わせる
Monad: 前の結果に依存して次を選ぶ
14-1. Functor
class Functor f where
fmap :: (a -> b) -> f a -> f b
ghci> fmap (+1) (Just 5)
Just 6
ghci> fmap (+1) [1,2,3]
[2,3,4]
ghci> (+1) <{{CONTENT}}gt; Just 5 -- <{{CONTENT}}gt; はfmapの演算子版
Just 6
14-2. Applicative
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
ghci> Just (+) <*> Just 1 <*> Just 2
Just 3
ghci> (+) <{{CONTENT}}gt; Just 1 <*> Just 2
Just 3
14-3. Monad(再掲)
class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
14-4. なぜ階層化されているか
Functor: 値を変換だけ
Applicative: 独立した複数の計算を組み合わせ
Monad: 前の結果に依存する計算
「できるだけ弱い抽象を使う」のがHaskellの文化。Monadで書けても、Applicativeで十分ならApplicative。
14-5. このセクションのまとめ
- Functor < Applicative < Monadの階層
- fmap / <*> / >>= が中核
- <{{CONTENT}}gt; = fmapの演算子版
- 「文脈の中で値を扱う」抽象
- 弱い抽象を使う規律
15. ジェネリクスとparametric polymorphism
identity :: a -> a
identity x = x
length' :: [a] -> Int
length' [] = 0
length' (_:xs) = 1 + length' xs
a は 型変数。Java/C# のジェネリクスと違い、型クラス制約なしの完全多相が普通。
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
実装側は 型を全く知らない。これが「parametricity」の力で、可能な実装が極端に絞られる(parametricityの自由定理)。
16. 型クラスと制約
sortList :: Ord a => [a] -> [a]
sortList = sort
uniqueAdds :: (Eq a, Num a) => [a] -> [a]
uniqueAdds = ...
-- 自作型クラス
class Greet a where
greet :: a -> String
instance Greet String where
greet s = "Hello, " ++ s
instance Greet Int where
greet n = "Number " ++ show n
17. 例外とエラー処理
17-1. 関数的エラー処理(推奨)
-- Maybe / Eitherで表現
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)
parseNum :: String -> Either String Int
parseNum s = ...
17-2. 例外
import Control.Exception
main :: IO ()
main = do
result <- try (readFile "missing.txt") :: IO (Either IOException String)
case result of
Left e -> putStrLn $ "Error: " ++ show e
Right c -> putStrLn c
-- catch
main = readFile "missing.txt" `catch` handler
where handler e = return ("default: " ++ show (e :: IOException))
17-3. このセクションのまとめ
- Maybe / Eitherで関数的エラー(推奨)
- IO例外はtry / catch
- error / undefinedはクラッシュ系(避ける)
18. レコード構文
data Person = Person
{ name :: String
, age :: Int
, email :: String
} deriving (Show)
alice :: Person
alice = Person { name = "Alice", age = 30, email = "a@b.c" }
ghci> name alice
"Alice"
-- 更新(新しいインスタンス)
older = alice { age = 31 }
レコード構文で自動的に getter が定義される。
19. モジュールとパッケージ
-- Lib.hs
module Lib
( doubleAll
, sumOfSquares
) where
doubleAll :: [Int] -> [Int]
doubleAll = map (*2)
sumOfSquares :: [Int] -> Int
sumOfSquares = sum . map (^2)
-- Main.hs
import Lib (doubleAll, sumOfSquares)
import qualified Data.Map.Strict as Map
import Data.List (sort)
20. 並行・並列処理
20-1. forkIO(軽量スレッド)
import Control.Concurrent
main :: IO ()
main = do
forkIO $ putStrLn "from thread 1"
forkIO $ putStrLn "from thread 2"
threadDelay 1000000
数百万のスレッドが現実的。Goのgoroutine並みの軽量さ。
20-2. MVar(共有可変参照)
m <- newMVar 0
forkIO $ modifyMVar_ m (\x -> return (x + 1))
val <- readMVar m
20-3. async(高水準)
import Control.Concurrent.Async
main = do
a1 <- async (computeA)
a2 <- async (computeB)
r1 <- wait a1
r2 <- wait a2
print (r1 + r2)
-- concurrentlyで並行実行
(r1, r2) <- concurrently computeA computeB
20-4. parMap(並列)
import Control.Parallel.Strategies
result = parMap rdeepseq heavy [1..1000]
20-5. このセクションのまとめ
- forkIOで軽量スレッド
- MVar / TVar / IORefで共有
- asyncで高水準並行
- Strategiesで並列計算
- 純粋関数は安全に並行可能
21. STM(Software Transactional Memory)
「ロックなしの並行制御」。データベースのトランザクションを思想を持ち込んだ仕組み。
import Control.Concurrent.STM
main = do
acc1 <- atomically $ newTVar 100
acc2 <- atomically $ newTVar 0
-- 送金(アトミック)
atomically $ do
bal <- readTVar acc1
when (bal >= 50) $ do
writeTVar acc1 (bal - 50)
modifyTVar acc2 (+ 50)
atomically 内のすべての操作が アトミック。競合があれば自動リトライ。デッドロックなし。
STMの重要な点は、ロックを直接持たないことではなく、複数の共有変数に対する更新を一つの整合した単位として書けることである。銀行口座、在庫、ジョブキュー、接続プールのように「複数の値が同時に正しく変わる」必要がある場面で特に効く。
ただし、atomically の中で任意のIOはできない。これは制約ではあるが、設計上は大きな利点でもある。トランザクション内部を純粋な状態変更に近づけ、外部API呼び出し、ログ出力、ファイル操作などを外に出すことで、リトライされても壊れないコードになる。
実務で使うときは、TVar だけでなく TQueue, TBQueue, TMVar も合わせて覚えるとよい。キューを使えば、ワーカー間の仕事の受け渡しを自然に表現できる。retry と orElse は強力だが、待ち条件が複雑になりすぎると読み手が追えなくなるため、ドメイン名のついた小さな関数に分ける。
22. パフォーマンスとプロファイル
22-1. ベンチマーク(Criterion)
import Criterion.Main
main = defaultMain
[ bgroup "fib"
[ bench "10" $ whnf fib 10
, bench "20" $ whnf fib 20
]
]
統計込みで信頼性高い。
22-2. プロファイル
ghc -prof -fprof-auto -O2 main.hs
./main +RTS -p
main.prof に時間・メモリのプロファイル。
22-3. ヒーププロファイル
./main +RTS -hd # 型ごとのヒープ使用
./main +RTS -hc # 関数ごと
hp2ps main.hp # PostScript化
メモリリーク検出に。
22-4. このセクションのまとめ
- Criterionでベンチマーク
- -profでプロファイル
- -h* でヒーププロファイル
- 遅延評価のメモリ問題に要注意
23. テスト戦略
23-1. HUnit
import Test.HUnit
tests = TestList
[ TestCase $ assertEqual "add 1 2" 3 (1 + 2)
, TestCase $ assertBool "1 < 2" (1 < 2)
]
main = runTestTT tests
23-2. QuickCheck(property-based)
import Test.QuickCheck
prop_reverseInverse :: [Int] -> Bool
prop_reverseInverse xs = reverse (reverse xs) == xs
main = quickCheck prop_reverseInverse
-- +++ OK, passed 100 tests.
「ランダム入力で性質を検証」する革命的な手法。Haskellが発明し、他言語に広がった(Hypothesis、PropErなど)。
23-3. Hspec
import Test.Hspec
main = hspec $ do
describe "addition" $ do
it "adds two positive numbers" $
1 + 2 `shouldBe` 3
it "is commutative" $
property $ \x y -> (x :: Int) + y == y + x
RSpec風。多くのプロジェクトで採用。
23-4. このセクションのまとめ
- HUnit: xUnit風
- QuickCheck: property-based
- Hspec: RSpec風(推奨)
- 純粋関数だからテスト容易
24. GHC拡張
GHCは標準Haskell 2010に 多数の言語拡張を追加。実用上はほぼ必須。
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE RecordWildCards #-}
主要なもの:
OverloadedStrings: "..." をString以外(Text)でも使える
DeriveGeneric / DeriveFunctor: 自動導出を拡張
TypeApplications: @ で型を明示
LambdaCase: \caseでラムダ + パターン
RecordWildCards: Record { .. } でフィールド全部展開
GADTs: 一般化代数的データ型
TypeFamilies: 型関数
RankNTypes: より強い多相
25. 主要ライブラリ
text: 効率的なUnicode文字列
bytestring: バイト列
containers: Map / Set / IntMap / Seq
unordered-containers: ハッシュベース
vector: 可変長配列
mtl: モナドトランスフォーマー
transformers: モナドトランスフォーマー
aeson: JSON
http-client / http-conduit: HTTP
servant: 型安全Web API
yesod / scotty / spock: Webフレームワーク
persistent: ORM
async: 並行
stm: STM
parallel: データ並列
criterion: ベンチマーク
QuickCheck: property test
hspec / tasty: テスト
optparse-applicative: CLI引数
megaparsec: パーサコンビネータ
lens: レンズ(イミュータブル更新)
26. よくある落とし穴FAQ
Q1. Int と Integer の違い
Int: 機械語整数(速い、オーバーフロー注意)
Integer: 任意精度(遅いが正確)
Q2. String は遅い?
String = [Char] でリスト連結が遅い。Data.Text を使う(OverloadedStrings拡張併用)。
Q3. 遅延評価でメモリが膨らむ
foldl ではなく foldl'(正格)。Data.Map.Strict(遅延でない)。
Q4. モナドが分からない
「何も特別なことではない」。do 記法に慣れて、Maybe IO Either を順に使う。理論より使用例。
Q5. 型エラーが読めない
GHCのエラーは長い。HLS がインライン表示してくれる。慣れが必要。
Q6. ポイントフリー使いすぎ
過度なpointfreeは読みにくい。f x = ... の方が明示的。
Q7. 並行処理の安全性
純粋関数は 完全にスレッドセーフ。共有状態が必要なら MVar / TVar。
Q8. IORef vs MVar vs TVar
IORef: 非並行、単純な可変参照
MVar: 並行、ロック付き
TVar: STM、トランザクション
Q9. error / undefinedを使うべき?
避ける。Maybe / Either で安全に。
Q10. letとwhere
let ... in ...(前置)と f x = ... where ...(後置)。どちらでも好みで。
27. 実践パターン集
27-1. パイプライン
import Data.List
result = sort . filter (> 0) . map (*2) $ [1, -2, 3, -4]
-- [2, 6]
27-2. Maybeチェイン
import Data.Maybe (fromMaybe)
calc :: Maybe Int
calc = do
x <- safeDiv 10 2
y <- safeDiv x 2
return (y + 1)
result = fromMaybe 0 calc
27-3. 型レベル設計
data UserId = UserId Int
data ProductId = ProductId Int
-- 混同できない
fetch :: UserId -> IO User
「newtypeで意味的型安全」。
27-4. Functorで値変換
fmap (+1) (Just 5) -- Just 6
fmap (+1) [1,2,3] -- [2,3,4]
fmap (+1) (Right 5) -- Right 6
「コンテナの中身を変換」を統一インターフェースで。
27-5. このセクションのまとめ
- 関数合成 + $ でパイプライン
- Maybe / Eitherのdo記法でエラー処理
- newtypeで意味的型安全
- fmap / <*> / >>= で文脈付き計算
28. 学習ロードマップ(30日)
Week 1: 基礎
- GHCupインストール、ghci体験
- 関数定義、パターンマッチ、ガード
- リスト・タプル
- map / filter / fold
Week 2: 型
- 代数的データ型(ADT)
- 型クラス(Eq / Ord / Show)
- Maybe / Either
- 型推論を読む
Week 3: モナド
- IO / do記法
- Maybe / Either / Listモナド
- Functor / Applicative / Monadの階層
- 自作IOプログラム
Week 4: 実践
29. 用語集
あ行
- アトム: 最小単位(式)
- 遅延評価: 必要になるまで評価しない
- イミュータブル: 変更不可
か行
- カリー化(currying): f(a,b) をf(a)(b) に
- 関数合成: f . g
- クロージャ: 環境を捕捉する関数
さ行
- 参照透明性: 式 = 値、文脈に依存しない
- 正格評価: 即座に評価
- 総称関数: ジェネリクス
た行
な行
は行
- パーサコンビネータ: パーサを組み合わせる関数群
- パターンマッチ: 型を分解
- 不変: イミュータブル
ま行
- モナド: 文脈付き計算
- モナドトランスフォーマー: モナドを重ねる
や〜わ行
- 遅延データ構造: 必要分だけ計算
A〜Z
- ADT: Algebraic Data Type
- GADT: Generalized ADT
- GHC: Glasgow Haskell Compiler
- HKT: Higher-Kinded Type
- HLS: Haskell Language Server
- MTL: Monad Transformer Library
- STM: Software Transactional Memory
- WHNF: Weak Head Normal Form
発展: 型と関数型設計
ここからはHaskellの各機能を 実例とともに深掘り。モナド、型クラス、遅延評価、並行性、Web開発、性能チューニングまで詳細に。
31. 純粋関数型の哲学詳細
31-1. 参照透明性の威力
-- 純粋関数の実例
square :: Int -> Int
square x = x * x
-- どこに置いても結果同じ
let a = square 5 -- 25
let b = square 5 -- 25
a == b -- True、必ず
-- 等式推論で書き換え可能
square 5 + square 5
= 25 + 25
= 50
「式 = 値」が常に成立。コードを 数学のように書き換えられる。
31-2. 副作用は型で表す
-- 純粋(副作用なし)
add :: Int -> Int -> Int
-- IO副作用(外部とやり取り)
greet :: String -> IO ()
-- 状態変更(Stateモナド)
counter :: State Int Int
-- エラー(Eitherモナド)
parse :: String -> Either Error Int
-- 非決定性(Listモナド)
combinations :: [Int] -> [[Int]]
型を見るだけで「この関数が何ができるか」が分かる。
31-3. このセクションのまとめ
- 参照透明性で等式推論
- 副作用は型で明示
- 純粋関数はテスト容易
- 並行処理が安全
32. 型システム深掘り
32-1. 型推論の威力
ghci> :t (\x -> x + 1)
(\x -> x + 1) :: Num a => a -> a
ghci> :t map
map :: (a -> b) -> [a] -> [b]
ghci> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
ML系の Hindley-Milner型推論。多くの場合、型注釈なしで動く。
32-2. 多相と型クラス
-- パラメータ多相(任意の型)
identity :: a -> a
identity x = x
-- 制約付き多相
add :: Num a => a -> a -> a
add x y = x + y
-- 複数制約
process :: (Num a, Show a) => a -> String
process x = "Got: " ++ show (x + 1)
32-3. 型クラス階層
Eq ==, /=
Ord <, <=, >, >=, compare
Show show
Read read
Num +, -, *, fromInteger, abs
Real toRational
Integral div, mod, toInteger
RealFrac truncate, round, ceiling, floor
Fractional /, recip, fromRational
Floating pi, exp, log, sin, cos, sqrt
Enum succ, pred, [..], fromEnum, toEnum
Bounded minBound, maxBound
Functor fmap
Applicative pure, <*>
Monad return, >>=
Foldable foldr, length
Traversable traverse
32-4. 多相再帰
-- ジェネリックなlength
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
a の型を一切知らずに動く。これがparametric polymorphismの力。
32-5. このセクションのまとめ
- Hindley-Milner型推論
- パラメータ多相 + 型クラス
- ad-hoc多相は型クラス制約で
- 階層: Functor < Applicative < Monad
33. モナド完全解説
33-1. モナドが「特別」なのはなぜか
「モナドは難しい」と言われるが、実態は 「文脈付き値を扱う共通インターフェース」。
Maybe a: 値があるかも、ないかも
[a]: 複数の可能な値
IO a: IO副作用を持つ計算でaを返す
State s a: 状態sを持つ計算でaを返す
Either e a: 失敗eまたは成功a
Reader r a: 環境rからaを計算
Writer w a: ログwを伴う計算でaを返す
これらすべてが 「>>= で連結できる」という共通点を持つ。
33-2. >>= の意味
(>>=) :: Monad m => m a -> (a -> m b) -> m b
-- Maybeの場合
Just 5 >>= \x -> Just (x + 1) -- Just 6
Nothing >>= \x -> Just (x + 1) -- Nothing(短絡)
-- Listの場合
[1, 2, 3] >>= \x -> [x, -x] -- [1,-1,2,-2,3,-3]
-- IOの場合
getLine >>= \s -> putStrLn s -- 入力を出力
「前の結果に依存して次の計算を選ぶ」。
33-3. do記法の脱糖
-- do記法
do x <- foo
y <- bar x
return (x + y)
-- 等価
foo >>= \x ->
bar x >>= \y ->
return (x + y)
すべての do は >>= の連鎖の 糖衣。
33-4. 自作モナド:State
import Control.Monad.State
counter :: State Int Int
counter = do
n <- get
put (n + 1)
return n
evalState (do
a <- counter
b <- counter
c <- counter
return [a, b, c]) 0
-- [0, 1, 2]
「状態を持つ計算」をモナドで表現。Pureな世界で「変数」のような体験。
33-5. このセクションのまとめ
- モナドは「文脈付き計算の連結」
- >>= で「前の結果 → 次の計算」
- do記法は >>= の糖衣
- Maybe / List / IO / State / Eitherすべてモナド
- 「難しい」は誤解、習慣で慣れる
34. 型クラス深掘り
34-1. 自作型クラス
class Greeter a where
greet :: a -> String
shout :: a -> String
shout x = map toUpper (greet x) -- デフォルト実装
instance Greeter String where
greet s = "Hello, " ++ s
instance Greeter Int where
greet n = "Number " ++ show n
greet "Alice" -- "Hello, Alice"
greet (42 :: Int) -- "Number 42"
shout "Bob" -- "HELLO, BOB"
34-2. multi-parameter type classes
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
class Convert a b where
convert :: a -> b
instance Convert Int String where
convert n = show n
instance Convert String Int where
convert s = read s
複数の型をパラメータに持つ型クラス。
34-3. 型族(Type Families)
{-# LANGUAGE TypeFamilies #-}
class Container c where
type Elem c
empty :: c
insert :: Elem c -> c -> c
instance Container [a] where
type Elem [a] = a
empty = []
insert x xs = x : xs
「型レベル関数」。associated typeともいう。
34-4. 高階型クラス(HKT)
class Functor f where
fmap :: (a -> b) -> f a -> f b
f は 型コンストラクタ(Maybe、[]、IO など)。Java/C# では表現できない型抽象。
34-5. このセクションのまとめ
- class / instanceでad-hoc多相
- デフォルト実装可
- multi-parameter / type family / HKT
- Javaのinterfaceより遥かに柔軟
35. 遅延評価の詳細
35-1. WHNF(Weak Head Normal Form)
-- リストは「先頭だけ評価」が基本
let xs = [1..1000000] -- まだ計算されていない
take 5 xs -- ここで先頭5個だけ計算
「weak head normal form」 = データコンストラクタが見える状態。
35-2. seqと $!(正格化)
-- 通常(遅延)
sum [1..1000000] -- 巨大なthunkが積み上がる
-- seqで正格
seq x y -- xを評価してからyを返す
-- $! で関数引数を正格に
f $! x -- xを評価してからfを呼ぶ
-- foldl' で正格畳み込み
foldl' (+) 0 [1..1000000] -- 効率的
35-3. bang patterns
{-# LANGUAGE BangPatterns #-}
sumStrict :: [Int] -> Int
sumStrict = go 0
where go !acc [] = acc
go !acc (x:xs) = go (acc + x) xs
! で その場で評価を強制。
35-4. 遅延評価のメリット
-- 無限リスト
naturals = [0..]
take 10 naturals -- [0,1,2,3,4,5,6,7,8,9]
-- 短絡
firstNonZero :: [Int] -> Int
firstNonZero xs = head (filter (/= 0) xs)
firstNonZero [0, 0, 5, undefined] -- 5(undefinedは評価されない)
-- データ構造の自然な定義
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]
35-5. このセクションのまとめ
- 必要になるまで評価しない
- thunkの積み上がりに注意
- foldl' / bang patternで正格化
- 無限リスト / 短絡が自然に書ける
- メモリリーク要因にもなる
36. 並行・並列処理
36-1. forkIO(軽量スレッド)
import Control.Concurrent
main = do
forkIO $ do
threadDelay 1000000
putStrLn "from thread 1"
forkIO $ do
threadDelay 500000
putStrLn "from thread 2"
threadDelay 2000000
GHCの 「グリーンスレッド」。数百万起動可能。Goのgoroutine並み。
36-2. MVar
import Control.Concurrent.MVar
main = do
m <- newMVar 0
forkIO $ modifyMVar_ m (return . (+1))
forkIO $ modifyMVar_ m (return . (+1))
threadDelay 100000
val <- readMVar m
print val
「ロック付きの可変参照」。
36-3. STM(Software Transactional Memory)
import Control.Concurrent.STM
main = do
acc1 <- atomically $ newTVar 100
acc2 <- atomically $ newTVar 0
-- 送金(アトミック、競合があれば自動リトライ)
atomically $ do
bal <- readTVar acc1
when (bal < 50) retry
modifyTVar acc1 (subtract 50)
modifyTVar acc2 (+ 50)
「ロックフリーで安全な並行」。データベースのトランザクション思想を取り入れた革新的機能。
36-4. async(高水準)
import Control.Concurrent.Async
main = do
a <- async (computeA)
b <- async (computeB)
resultA <- wait a
resultB <- wait b
print (resultA + resultB)
-- concurrentlyで並行
(a, b) <- concurrently computeA computeB
-- mapConcurrentlyでリスト並行
results <- mapConcurrently fetch urls
36-5. parMap(データ並列)
import Control.Parallel.Strategies
results = parMap rdeepseq heavyFn [1..1000]
-- 複数CPUで並列実行
36-6. このセクションのまとめ
- forkIOで軽量スレッド(数百万)
- MVar / TVar / IORefで共有
- STMでロックフリー並行
- asyncで高水準合成
- parMapでデータ並列
37. HaskellのWeb開発
37-1. servant(型安全Web API)
{-# LANGUAGE DataKinds, TypeOperators #-}
import Servant
type API = "users" :> Get '[JSON] [User]
:<|> "users" :> Capture "id" Int :> Get '[JSON] User
:<|> "users" :> ReqBody '[JSON] CreateUser :> Post '[JSON] User
server :: Server API
server = listUsers :<|> getUser :<|> createUser
where
listUsers = liftIO $ readUsers
getUser id = liftIO $ findUser id
createUser req = liftIO $ insertUser req
main = run 8080 (serve api server)
「型レベルでAPIを表現」。クライアントコード自動生成、ドキュメント生成も可能。
37-2. yesod / scotty / spock
-- scotty(軽量、Sinatra風)
import Web.Scotty
main = scotty 3000 $ do
get "/" $ html "Hello"
get "/users/:id" $ do
id <- param "id"
json (User id "Alice")
37-3. このセクションのまとめ
- servant: 型安全API(業界標準)
- yesod: フルスタック
- scotty: 軽量
- spock: 中間
38. パフォーマンスチューニング
38-1. プロファイル
ghc -prof -fprof-auto -O2 main.hs
./main +RTS -p
# main.profに時間プロファイル
./main +RTS -hd # ヒーププロファイル
./main +RTS -hc # cost-center別
hp2ps main.hp # PostScript化
38-2. ベンチマーク(Criterion)
import Criterion.Main
main = defaultMain
[ bgroup "fib"
[ bench "10" $ whnf fib 10
, bench "20" $ whnf fib 20
]
]
統計込みで信頼性高い。
38-3. 高速化のテクニック
- foldl' / foldl1' で正格畳み込み
- Data.Map.Strict(遅延でない)
- Data.ByteString / Text(巨大文字列)
- bang patternでthunk削減
- INLINEプラグマで関数インライン化
- UNPACKプラグマでboxing排除
data Point = Point !Double !Double -- bangで正格
{-# INLINE myFn #-}
myFn :: Int -> Int
myFn x = x * 2
38-4. このセクションのまとめ
- profileで測定(+RTS -p / -h)
- Criterionでベンチマーク
- 正格性でthunk削減
- ByteString / Textで文字列高速化
- INLINE / UNPACKプラグマ
39. テスト戦略
39-1. HSpec(BDD)
import Test.Hspec
main = hspec $ do
describe "addition" $ do
it "adds two numbers" $
1 + 2 `shouldBe` 3
it "handles negatives" $
(-1) + (-1) `shouldBe` (-2)
describe "string operations" $ do
it "reverses correctly" $
reverse "abc" `shouldBe` "cba"
39-2. QuickCheck(Property-based)
import Test.QuickCheck
prop_reverseInverse :: [Int] -> Bool
prop_reverseInverse xs = reverse (reverse xs) == xs
prop_lengthAfterMap :: [Int] -> Bool
prop_lengthAfterMap xs = length (map (+1) xs) == length xs
main = do
quickCheck prop_reverseInverse
quickCheck prop_lengthAfterMap
「ランダム入力で性質検証」。Haskellが 発明した手法で、他言語に広がった(Hypothesis、PropErなど)。
39-3. tasty(テストフレームワーク)
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
main = defaultMain $ testGroup "Tests"
[ testCase "1+1=2" $ 1+1 @?= 2
, testProperty "reverse" prop_reverseInverse
]
HUnit + QuickCheck + その他を 統合できる。
39-4. このセクションのまとめ
- HSpecでBDD
- QuickCheckでproperty-based(強力)
- tastyで統合
- 純粋関数だからテスト超容易
40. GHC拡張(モダンHaskell)
{-# LANGUAGE OverloadedStrings #-} -- "..." をText等にも
{-# LANGUAGE DeriveGeneric #-} -- Generic自動導出
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE LambdaCase #-} -- \caseでパターンマッチ
{-# LANGUAGE TupleSections #-} -- (1,) で部分適用
{-# LANGUAGE RecordWildCards #-} -- {..} で全フィールド
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeApplications #-} -- @Intで型明示
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GADTs #-} -- 一般化代数的データ型
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-} -- より強い多相
{-# LANGUAGE ConstraintKinds #-}
業務Haskellでは 多数の拡張を有効化するのが標準。
41. 主要ライブラリ詳細
text: 効率的Unicode文字列(Stringの代替)
bytestring: バイト列
containers: Map / Set / Seq / IntMap
unordered-containers: ハッシュベース
vector: 可変長配列
mtl / transformers: モナドトランスフォーマー
aeson: JSON
http-client: HTTP
servant: 型安全Web API
yesod / scotty: Webフレームワーク
persistent: ORM
async: 並行
stm: STM
parallel: データ並列
criterion: ベンチマーク
QuickCheck / hspec / tasty: テスト
optparse-applicative: CLI引数
megaparsec: パーサコンビネータ
lens: レンズ(イミュータブル更新)
ekg: メトリクス
katip: ロギング
42. lensの威力
import Control.Lens
data Address = Address
{ _street :: String
, _city :: String
} deriving (Show)
makeLenses ''Address
data Person = Person
{ _name :: String
, _addr :: Address
} deriving (Show)
makeLenses ''Person
let alice = Person "Alice" (Address "1st St" "Tokyo")
-- 取得
alice ^. addr . city -- "Tokyo"
-- 更新(新しいインスタンス)
alice & addr . city .~ "Osaka"
-- 関数で更新
alice & name %~ map toUpper
「ネスト構造の更新を関数的に」。ReactのImmer相当。
43. Parser Combinators(megaparsec)
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
type Parser = Parsec Void String
number :: Parser Int
number = L.decimal
addExpr :: Parser Int
addExpr = do
a <- number
_ <- char '+'
b <- number
return (a + b)
parseTest addExpr "1+2" -- 3
「パーサを関数として組み立てる」のがHaskell流。yacc/bison不要。
44. Haskell拡張FAQ
Q1. なぜHaskellは遅いと言われる?
実は遅くない。naïveなコードは遅いが、正格性 + コンパイラ最適化で Cに近い性能が出る。
Q2. モナドが分からない
「何も特別なことではない」。do 記法を使い、Maybe / IO / Either から始めれば、抽象化は後で見えてくる。
Q3. 型エラーが読めない
GHCのエラーは長い。HLS のインライン表示で慣れる。-XFlexibleContexts 等の拡張を使うとさらに難しくなる。
Q4. 学習リソース
- Learn You a Haskell(無料、入門)
- Real World Haskell(無料)
- Programming in Haskell(Hutton)
- Haskell Programming from First Principles
- Haskell.orgのwiki
Q5. 業務で使える?
使える。Pandoc、Shellcheck、Cardano、Standard Charteredなど実例多数。学習投資の元は取れる。
Q6. Scala / OCamlと比較
Haskell: 純粋、遅延、研究寄り
Scala: JVM、不純、業務寄り
OCaml: 正格、不純、関数型 + OOP
Q7. 並行処理は強い?
非常に強い。STM は他言語にない強力な機能。Erlang に並ぶ並行性の言語。
Q8. なぜ業界で広まらない?
学習曲線が急峻。「動くまで時間がかかる」。しかし 動けばバグが少ない。
Q9. MaybeよりEither?
エラー情報を伝えたいならEither。単純な「あるかも」ならMaybe。
Q10. Cabal vs Stack
最近はCabal v2で十分。新規はCabal、再現性重視でStack。
応用: 抽象化とプロジェクト設計
46. モナドトランスフォーマー
複数のモナドを 重ねて使う仕組み。
import Control.Monad.State
import Control.Monad.Except
-- State + Eitherの組み合わせ
type App a = StateT Int (Either String) a
incr :: App ()
incr = modify (+1)
errorIfBig :: App ()
errorIfBig = do
n <- get
when (n > 10) $ throwError "Too big"
run :: App () -> Either String ((), Int)
run action = runStateT action 0
mtl パッケージが標準。
46-1. 主要なトランスフォーマー
StateT s状態を持つ
ReaderT r環境を読む
WriterT wログを書く
ExceptT e例外
MaybeT Maybeを重ねる
46-2. このセクションのまとめ
- モナドを重ねるトランスフォーマー
- StateT / ReaderT / WriterT / ExceptT
- mtlが標準
- effect system(fused-effects、polysemy)も選択肢
47. Effect System
mtl の代替として注目される effect system。
-- polysemy
import Polysemy
import Polysemy.State
countDown :: Member (State Int) r => Sem r Int
countDown = do
n <- get
if n <= 0
then return 0
else do put (n - 1); countDown
mtl よりパフォーマンスや合成性に優れる。研究と実装が進行中。
Effect Systemの狙いは、副作用を「全部IOに入れる」のではなく、ログ、状態、例外、環境参照、外部APIなどの能力を型で分けて表現することにある。関数の型を見るだけで、その関数が何をできるかが分かるため、テスト時には本物のDBではなくメモリ実装を差し替えやすい。
mtl, polysemy, fused-effects, effectful は似た問題を扱うが、書き味、型推論、性能、エコシステムが異なる。アプリケーション全体で統一するなら、チームが読める抽象度を優先する。小さなプロジェクトでは ReaderT Env IO で十分なことも多く、Effect Systemを導入する価値は、差し替えたい効果が増えてから判断しても遅くない。
導入時に避けたいのは、すべての関数を抽象的な効果制約だらけにすること。境界に近い層では効果を持たせ、ドメインロジックは普通の純粋関数に寄せる。この分担ができていると、Effect Systemは複雑さではなく設計の見通しを与える。
48. Lens詳細
import Control.Lens
data User = User
{ _name :: String
, _age :: Int
, _addr :: Address
} deriving Show
data Address = Address
{ _city :: String
, _zip :: String
} deriving Show
makeLenses ''User
makeLenses ''Address
let alice = User "Alice" 30 (Address "Tokyo" "100-0001")
-- 取得
alice ^. name -- "Alice"
alice ^. addr . city -- "Tokyo"
-- 更新(新インスタンス)
alice & age .~ 31
alice & addr . city .~ "Osaka"
-- 関数で更新
alice & age %~ (+1)
alice & name %~ map toUpper
-- Traversal(複数要素)
let users = [alice, alice]
users ^.. traverse . name -- ["Alice", "Alice"]
users & traverse . age %~ (+1)
「深いネスト構造の更新を関数的に」。React Immerの元ネタ。
49. Type-level Programming
{-# LANGUAGE GADTs, DataKinds, KindSignatures #-}
data Nat = Z | S Nat
data Vec :: Nat -> * -> * where
VNil :: Vec 'Z a
VCons :: a -> Vec n a -> Vec ('S n) a
-- 長さを型で保証
head' :: Vec ('S n) a -> a
head' (VCons x _) = x
-- 長さ0のベクタには使えない(コンパイルエラー)
型レベルで長さを表現できる。Haskellの型システムはIdrisやAgdaに近い表現力。
50. Haskellの歴史と未来
1990 Haskell 1.0
1998 Haskell 98
2010 Haskell 2010
GHCが事実上の標準実装
近年の動き:
- GHCの継続的改善
- Linear Types(リソース管理、Rust風)
- Dependent Types(型レベル計算強化)
- Cardano(暗号通貨)の本番採用
- Standard Chartered(金融)
- GitHub Semantic
- Hasura(GraphQL)
51. 完全なプロジェクト構成
my-haskell-project/
├── package.yaml or my-project.cabal
├── stack.yaml or cabal.project
├── README.md
├── LICENSE
├── app/
│ └── Main.hs
├── src/
│ ├── Lib.hs
│ └── Lib/
│ ├── Core.hs
│ ├── Types.hs
│ └── Utils.hs
├── test/
│ └── Spec.hs
└── benchmark/
└── Bench.hs
52. Haskell学習ロードマップ
Week 1: 基礎
- GHCupインストール
- 関数定義、パターンマッチ、ガード
- リスト、タプル
- map / filter / fold
Week 2: 型
- 代数的データ型(ADT)
- 型クラス
- Maybe / Either
- 多相
Week 3: モナド
- IO / do記法
- Maybe / List / Stateモナド
- Functor / Applicative / Monad
- 自作IOプログラム
Week 4: 実践
Week 5: 上級
- モナドトランスフォーマー
- lens
- 並行処理(STM、async)
- パフォーマンス(Criterion)
Week 6: 応用
- servantで型安全API
- persistentでDB
- megaparsecでパーサ
- 自作ライブラリ公開(Hackage)
実践: ランタイムとコミュニティ
54. 実装パターン集
54-1. StateとReaderの組み合わせ
import Control.Monad.RWS
type App = RWS Config [LogEntry] AppState
doWork :: App ()
doWork = do
config <- ask -- Reader(環境)
state <- get -- State
tell [LogEntry "started"] -- Writer
...
54-2. Pipe / Conduit(ストリーミング)
import Conduit
main = runConduit $
sourceFile "input.txt"
.| decodeUtf8C
.| linesUnboundedC
.| mapC processLine
.| sinkFile "output.txt"
巨大データを メモリ効率良く処理。
54-3. Freeモナド
{-# LANGUAGE DeriveFunctor #-}
data ConsoleF a
= WriteLine String a
| ReadLine (String -> a)
deriving Functor
type Console = Free ConsoleF
writeLine :: String -> Console ()
writeLine s = liftF (WriteLine s ())
readLine :: Console String
readLine = liftF (ReadLine id)
prog :: Console ()
prog = do
writeLine "Name?"
name <- readLine
writeLine ("Hello, " ++ name)
「作用を抽象化」して、後でインタプリタで解釈。テスト容易・並行可能・記録可能。
54-4. このセクションのまとめ
- RWSでState + Reader + Writer
- Conduitでストリーミング
- Freeモナドで作用抽象化
- 「作用を値として扱う」哲学
55. Haskellランタイム詳細
GHCランタイム:
- グリーンスレッド(数百万)
- 世代別GC
- 並行GC
- STMサポート
- FFI(C関数呼び出し)
設定:
./prog +RTS -N4 -- 4コア使用
./prog +RTS -H1G -- 初期ヒープ1GB
./prog +RTS -K100M -- スタック上限
56. Haskellのキャリア
業務利用例:
Standard Chartered(金融、巨大コードベース)
GitHub(Semantic、コード解析)
Mercury(金融ツール)
Cardano / IOG(暗号通貨)
Hasura(GraphQLバックエンド)
Tweag, Well-Typed(コンサル)
求人:
少ないが、いるところは確実にいる
給与は高い傾向
リモート可が多い
57. Haskellコミュニティ
書籍
- 『Programming in Haskell』Hutton
- 『Haskell Programming from First Principles』
- 『Real World Haskell』
- 『Parallel and Concurrent Programming in Haskell』Marlow
Web
- haskell.org(公式)
- learnyouahaskell.com(無料)
- haskell.foundation
- /r/haskell
コミュニティ
- ICFP(年次カンファレンス)
- ZuriHac、HaskellX
- Haskell Discord / Discourse
- Haskell Weekly newsletter
58. Haskellの哲学
- 純粋性
- 型による証明
- 等式推論
- Lazy by default
- 機能の最小化(Less is more)
- 数学的厳密性
-- これは関数定義であり、数学の方程式でもある
square :: Int -> Int
square x = x * x
-- どこで呼んでも同じ意味
-- リファクタリングが安全
-- テストが容易
参照: 型クラスと実例
60. 主要な型クラスのデフォルト実装
-- Eqの最小実装
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)
-- Ordの最小実装
class Eq a => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>), (>=) :: a -> a -> Bool
max, min :: a -> a -> a
-- compareを実装すれば残りはデフォルト
-- Functorの法則
fmap id = id
fmap (f . g) = fmap f . fmap g
-- Monadの法則
return a >>= k = k a
m >>= return = m
(m >>= k) >>= h = m >>= (\x -> k x >>= h)
61. 完全なクライアント・サーバ例(servant)
{-# LANGUAGE DataKinds, TypeOperators, DeriveGeneric #-}
import Servant
import Servant.Client
import Network.Wai.Handler.Warp (run)
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)
-- 型
data User = User { userId :: Int, userName :: String }
deriving (Show, Generic)
instance ToJSON User
instance FromJSON User
-- API
type API = "users" :> Get '[JSON] [User]
:<|> "users" :> Capture "id" Int :> Get '[JSON] User
api :: Proxy API
api = Proxy
-- サーバ
server :: Server API
server = listUsers :<|> getUser
where
listUsers = return [User 1 "Alice", User 2 "Bob"]
getUser id = return (User id "Unknown")
app :: Application
app = serve api server
-- クライアント(自動生成!)
listUsers :: ClientM [User]
getUser :: Int -> ClientM User
listUsers :<|> getUser = client api
-- メイン
main :: IO ()
main = run 8080 app
「API定義からServerとクライアント両方が自動生成」。Haskellの型システムの威力。
62. 完全な並行プログラム例
import Control.Concurrent.STM
import Control.Concurrent.Async
import Control.Monad
main :: IO ()
main = do
-- 共有リソース
pool <- newTVarIO 100
log_ <- newTQueueIO
-- ワーカーを10個並行に
results <- forConcurrently [1..10] $ \i -> do
-- 銀行送金(アトミック)
success <- atomically $ do
bal <- readTVar pool
if bal >= 10
then do
writeTVar pool (bal - 10)
writeTQueue log_ ("Worker " ++ show i ++ " withdrew 10")
return True
else return False
return (i, success)
-- ログを出力
logs <- atomically $ flushTQueue log_
mapM_ putStrLn logs
print results
STM + asyncで デッドロックフリーな並行処理。
63. Haskellの現代的活用領域
今、Haskellが選ばれている分野:
- 暗号通貨(Cardano、Plutus言語)
- 金融バックエンド
- DSL / 言語処理系
- フォーマル検証
- GraphQLバックエンド(Hasura)
- コード解析(Semantic)
- パーサ(Pandoc)
- 数学処理系
- 学術研究
新興の分野:
- LLMの関数型API
- 分散システム
- 形式手法
64. Haskellプログラマの規律
コーディング規律:
☐ 純粋関数を優先、IOは境界に
☐ 型注釈で意図を表明
☐ サブモナド(Maybe / Either)で短絡
☐ pattern matchを網羅的に(warning有効)
☐ partial関数(head, !!)を避ける
☐ StringよりText / ByteString
☐ foldl' をfoldlの代わりに
☐ HSpec / QuickCheckでテスト
☐ HLintで品質チェック
65. Haskellリソース完全リスト
必読書
- 『Learn You a Haskell』(無料、入門)
- 『Programming in Haskell』Graham Hutton(中級)
- 『Haskell Programming from First Principles』(実践)
- 『Real World Haskell』(無料、実用)
- 『Parallel and Concurrent Programming in Haskell』Simon Marlow
上級
- 『Type-Driven Development with Idris』
- 『Algebra-Driven Design』
- 『Thinking Functionally with Haskell』
- 『Pearls of Functional Algorithm Design』
Web
- haskell.org / wiki
- haskell-lang.org
- school.alchemists.io(有料)
- haskellbookclub
- Hackage(パッケージ)
- Hoogle(型検索)
- LambdaCast(podcast)
66. Haskell学習の心得
1. 最初の壁「モナド」を恐れない
- do記法から始めれば自然に
- Maybe → IO → Eitherの順で
2. 型エラーは友達
- HLSのインライン表示
- エラーは小さくして特定
3. 純粋性を体感
- 副作用をIOに閉じ込める
- 残りは数学的に正しい
4. 標準を覚える
- Functor / Applicative / Monadの階層
- mapM_、forM_、mapMなどの慣用句
5. 業務レベルへ
- mtl / lens / aeson / servant
- Cabal / Stack
- HLint / haskell-language-server
運用: ツールとプロダクション
68. cabal / stack詳細
68-1. cabalプロジェクト
cabal-version: 3.0
name: my-project
version: 0.1.0.0
synopsis: A short description
description: Detailed description
license: BSD-3-Clause
license-file: LICENSE
author: Your Name
maintainer: you@example.com
build-type: Simple
common shared
default-language: Haskell2010
ghc-options: -Wall -Wcompat
build-depends: base >= 4.14 && < 5
library
import: shared
exposed-modules: Lib
other-modules: Lib.Internal
hs-source-dirs: src
build-depends: text, containers
executable my-app
import: shared
main-is: Main.hs
hs-source-dirs: app
build-depends: my-project
test-suite my-tests
import: shared
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs: test
build-depends: my-project, hspec
68-2. stackプロジェクト
# stack.yaml
resolver: lts-22.0
packages:
- .
extra-deps: []
flags: {}
LTSスナップショットで 再現性を保証。
69. Web開発の選択肢
69-1. Servant(型安全)
-- 型レベルでAPIを表現
type API = "users" :> Get '[JSON] [User]
:<|> "users" :> Capture "id" Int :> Get '[JSON] User
:<|> "users" :> ReqBody '[JSON] CreateUser :> Post '[JSON] User
業界標準。
69-2. Yesod
「フルスタックWebフレームワーク」。Rails風だが型安全。
69-3. Scotty
import Web.Scotty
main = scotty 3000 $ do
get "/" $ html "Hello"
get "/users/:id" $ do
id <- param "id"
json (User (id :: Int) "Alice")
軽量、Sinatra風。
69-4. このセクションのまとめ
- Servant: 型安全API(業界標準)
- Yesod: フルスタック
- Scotty: 軽量
- Spock: 中間
70. データベース連携
70-1. persistent + esqueleto
import Database.Persist.TH
import qualified Database.Esqueleto.Experimental as E
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
name String
email String
UniqueEmail email
deriving Show
|]
-- クエリ
fetchUsers = E.select $ do
user <- E.from $ E.table @User
E.where_ (user E.^. UserEmail E.like (E.val "%@example.com"))
return user
「Template Haskell」でスキーマからモデル生成。
70-2. Beam(型安全SQL)
data UserT f = User
{ _userId :: Columnar f Int
, _userName :: Columnar f Text
, _userEmail :: Columnar f Text
} deriving Generic
-- 型安全なクエリ
fetchByEmail email = runSelectReturningOne $
select $ filter_ (\u -> _userEmail u ==. val_ email) (all_ (_users db))
70-3. このセクションのまとめ
- persistent: ORM
- esqueleto: SQL DSL
- beam: 型安全SQL
- HDBC: 低レベル
- postgresql-simple: PostgreSQLクライアント
71. CLIツール開発
import Options.Applicative
data Options = Options
{ input :: FilePath
, output :: Maybe FilePath
, verbose :: Bool
}
opts :: Parser Options
opts = Options
<{{CONTENT}}gt; strArgument (metavar "INPUT")
<*> optional (strOption (long "output" <> short 'o'))
<*> switch (long "verbose" <> short 'v')
main :: IO ()
main = do
o <- execParser (info opts (progDesc "My tool"))
print o
optparse-applicative で 型安全なCLI引数解析。
72. デバッグツール
- Trace.traceでprintデバッグ
- Debug.Traceモジュール
- ghciの :breakでブレークポイント
- HLSのhoverで型確認
- ghc-mod / hieでエディタ統合
Haskellのデバッグは、値を逐次追うよりも「型、評価、境界」を切り分ける作業になりやすい。まずHLSやGHCiで型を確認し、次に最小の式へ分解し、最後に必要な場所だけ trace を入れる。trace は便利だが、純粋なコードに観測用の副作用を混ぜるため、調査が終わったら消す前提で使う。
評価順が原因で分かりにくいときは、seq, bang pattern, deepseq を試す前に、どのデータ構造が大きなサンクを保持しているかを疑う。プロファイルを取るなら、時間だけでなくメモリ保持も見る。Haskellでは「遅い」より先に「思ったより評価されず、思ったより保持される」ことが問題になる場合がある。
実務では、デバッガだけに頼らず、QuickCheckやHedgehogで性質をテストするほうが早いことも多い。特にパーサ、変換、正規化、状態遷移は、個別例より性質で押さえると不具合の発見率が上がる。
73. 完全なプロジェクト構成
my-haskell-project/
├── package.yaml # hpack(推奨、cabalを生成)
├── stack.yaml or cabal.project
├── README.md
├── LICENSE
├── ChangeLog.md
├── .gitignore
├── app/ # 実行可能ファイル
│ └── Main.hs
├── src/ # ライブラリコード
│ ├── Lib.hs
│ └── Lib/
│ ├── Core.hs
│ ├── Types.hs
│ └── Util.hs
├── test/ # テスト
│ ├── Spec.hs
│ └── Lib/
│ └── CoreSpec.hs
├── benchmark/ # ベンチマーク
│ └── Bench.hs
└── examples/
74. プロダクションHaskellの知見
規律:
- 厳格な型注釈(業務API)
- HLintで品質維持
- HSpec + QuickCheckの充実
- 適切な並行プリミティブ(async / STM)
- メトリクス(ekg-prometheus)
- 構造化ロギング(katip)
落とし穴:
- 遅延評価のメモリリーク
- StringとTextの混在
- 型エラーが長い
- コンパイル時間
- エコシステムの成熟度
補遺: 実例とFAQ
76. 完全な実例:JSON APIクライアント
{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}
module Main where
import Data.Aeson
import GHC.Generics
import Network.HTTP.Simple
import qualified Data.ByteString.Lazy.Char8 as BL
data Post = Post
{ userId :: Int
, id_ :: Int
, title :: String
, body :: String
} deriving (Show, Generic)
instance FromJSON Post where
parseJSON = withObject "Post" $ \o -> Post
<{{CONTENT}}gt; o .: "userId"
<*> o .: "id"
<*> o .: "title"
<*> o .: "body"
instance ToJSON Post
main :: IO ()
main = do
resp <- httpLBS "https://jsonplaceholder.typicode.com/posts/1"
case eitherDecode (getResponseBody resp) :: Either String Post of
Right post -> putStrLn $ "Title: " ++ title post
Left err -> putStrLn err
77. 完全な実例:簡易REPL
import System.IO
main :: IO ()
main = do
hSetBuffering stdout NoBuffering
loop
where
loop = do
putStr "> "
input <- getLine
case input of
"quit" -> putStrLn "Bye!"
"help" -> do
putStrLn "Commands: quit, help, hello"
loop
"hello" -> do
putStrLn "World!"
loop
_ -> do
putStrLn $ "Unknown: " ++ input
loop
78. 拡張FAQ
Q11. 文字列の選び方
String: [Char]、教育・小規模
Text: 効率的Unicode(推奨)
ByteString: バイナリ・速度重視
Data.Text.Lazy: 大きい文字列のストリーミング
Q12. IORef vs MVar vs TVar
IORef: 非並行、単純な可変参照
MVar: 並行、ロック付き(block可能)
TVar: STM、トランザクション
Q13. リストとシーケンス
[a]: 片方向、appendが遅い
Data.Sequence.Seq a: 両端O(1)
Data.Vector.Vector a: ランダムアクセスO(1)
Q14. MapとHashMap
Data.Map.Strict: 順序付き、O(log n)
Data.HashMap.Strict: ハッシュ、O(1) 平均
Data.IntMap.Strict: intキー特化
Q15. 並行の選び方
forkIO: 低レベル
async: 高水準(推奨)
STM: ロックフリー、複合操作
Q16. cabal v2 vs Stack
cabal v2: 標準、新規推奨
Stack: 再現性最強、CI向き
両者は併存
Q17. ghcid
ghcid --command="cabal repl"
ファイル変更を検知して 自動再コンパイル。エラーが即座に分かる。
Q18. HLint
hlint .
コード品質チェッカ。CI必須。
Q19. Haskellの成熟度
Web開発: 成熟(Servant、Yesod、Scotty)
DB: 成熟(persistent、postgresql-simple)
並行: 最強クラス(STM)
GUI: やや弱い(GTK、Threepenny)
ML: 研究用途中心(HMatrix、Haxl)
Q20. Haskellの哲学を一言で
“Avoid runtime errors. Use the type system.”
79. Haskell学習の進め方
Stage 1: 入門
- リスト・関数・パターンマッチ
- 型推論を読む
- map / filter / fold
Stage 2: 中級
- ADT / 型クラス
- Maybe / Either
- IOの理解
Stage 3: モナド
- do記法
- State / Reader / Writer
- Functor < Applicative < Monad
Stage 4: 上級
- モナドトランスフォーマー
- lens
- 並行処理(STM)
- 自作型クラス
Stage 5: マスター
- GADTs / Type Family
- Freeモナド
- effect system
- 型レベルプログラミング
- 自作DSL
上級: 型レベルとEffect
81. GADT(一般化代数的データ型)
{-# LANGUAGE GADTs #-}
data Expr a where
IntLit :: Int -> Expr Int
BoolLit :: Bool -> Expr Bool
Add :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
eval :: Expr a -> a
eval (IntLit n) = n
eval (BoolLit b) = b
eval (Add a b) = eval a + eval b
eval (If c t e) = if eval c then eval t else eval e
main = do
print (eval (Add (IntLit 1) (IntLit 2))) -- 3
print (eval (If (BoolLit True) (IntLit 10) (IntLit 20))) -- 10
82. 型レベルプログラミング
{-# LANGUAGE DataKinds, KindSignatures, GADTs #-}
data Nat = Z | S Nat
data Vec :: Nat -> * -> * where
VNil :: Vec 'Z a
VCons :: a -> Vec n a -> Vec ('S n) a
-- 長さを型で保証するhead
vhead :: Vec ('S n) a -> a
vhead (VCons x _) = x
-- 空でないことが型で保証されているのでパターンマッチ網羅
IdrisやCoqに近い表現力をHaskellでも。
83. effect system詳細
83-1. polysemy
import Polysemy
import Polysemy.State
countDown :: Member (State Int) r => Sem r ()
countDown = do
n <- get
if n <= 0
then return ()
else do
put (n - 1)
countDown
main = do
let (final, ()) = run $ runState 10 countDown
print final -- 0
mtlの代替として注目。
83-2. fused-effects
import Control.Carrier.State.Strict
countDown :: Has (State Int) sig m => m ()
countDown = do
n <- get
...
別のeffect system。Polysemyと競合。
84. lens完全活用
import Control.Lens
-- 既存型へのレンズ手書き
nameL :: Lens' Person String
nameL = lens _name (\p n -> p { _name = n })
-- Template Haskellで自動生成
makeLenses ''Person
-- 主要操作
person ^. name -- 取得
person & name .~ "Bob" -- 更新
person & name %~ map toUpper -- 関数で更新
person & age +~ 1 -- 数値演算
-- Traversal
[1,2,3] ^.. traverse -- [1,2,3]
[1,2,3] & traverse +~ 10 -- [11,12,13]
[Just 1, Nothing, Just 3] ^.. traverse . _Just -- [1, 3]
-- Prism(直和型)
data Shape = Circle Double | Square Double
makePrisms ''Shape
shape ^? _Circle -- Maybe Double
shape & _Circle .~ 5
85. Freeモナドの応用
{-# LANGUAGE DeriveFunctor #-}
data ConsoleF a
= WriteLine String a
| ReadLine (String -> a)
deriving Functor
type Console = Free ConsoleF
writeLine :: String -> Console ()
writeLine s = liftF (WriteLine s ())
readLine :: Console String
readLine = liftF (ReadLine id)
-- プログラム
prog :: Console ()
prog = do
writeLine "Name?"
name <- readLine
writeLine ("Hello, " ++ name)
-- 実行(IO)
runConsole :: Console a -> IO a
runConsole = foldFree $ \case
WriteLine s next -> putStrLn s >> return next
ReadLine cont -> getLine >>= return . cont
-- 実行(テスト用)
runPure :: [String] -> Console a -> ([String], a)
runPure inputs = ...
-- 同じプログラムを実用にもテストにも
86. パフォーマンスチューニング詳細
86-1. プロファイル取得
# 時間プロファイル
ghc -O2 -prof -fprof-auto main.hs
./main +RTS -p -RTS
# ヒーププロファイル
./main +RTS -hd # 型別
./main +RTS -hc # cost-center別
./main +RTS -hT # 形別
hp2ps main.hp
# RTS統計
./main +RTS -s
86-2. 最適化のポイント
1. 正格性
- foldl' をfoldlの代わりに
- bang patterns
- データ型のフィールドに !
2. データ型
- Text > String
- ByteString > String(バイナリ)
- Data.Map.Strict > Data.Map
- UNPACKプラグマ
3. 並行
- parMapで並列
- asyncで並行
- STMで安全な共有
4. インライン
- {-# INLINE foo #-}
- 小さい関数
87. Haskellのコミュニティ動向
活発な領域:
- Cardano(暗号通貨、Plutus言語)
- Hasura(GraphQL)
- GHC改善(Linear types、Dependent types)
- effect system(polysemy、fused-effects)
- StackのHaskell.orgへの移管
イベント:
ICFP(年次)
ZuriHac(年次)
HaskellX
地域のHaskell meetup
上級: コンパイラと最適化
89. Haskellコンパイラ詳細
GHCの処理流れ:
Haskellソース
↓ パース
AST
↓ rename
Renamed AST
↓ typecheck
Typechecked AST
↓ desugar
Core(中間表現)
↓ optimize(多数のパス)
Optimized Core
↓ STG(Spineless Tagless G-machine)
STG
↓ Cmm
Cmm(C--)
↓
ネイティブコードor LLVM IR
90. 遅延評価とStrictness解析
GHCは strictness analysis で「確実に評価される引数」を検出して、自動で正格化します。
-- 内部的にはGHCが解析
sumStrict :: Int -> [Int] -> Int
sumStrict acc [] = acc
sumStrict acc (x:xs) = sumStrict (acc + x) xs
-- GHCは `acc` が常に評価されると分かれば、自動で正格化(unboxing等)
遅延評価はHaskellの強みだが、性能面では常に意識が必要である。リストを一気に作らず必要な分だけ処理できる一方で、評価されていない式が積み上がるとメモリを圧迫する。典型例は左畳み込み、巨大なログ構築、MapやStateに未評価の値を入れ続ける処理である。
GHCのStrictness解析は多くのケースで賢く働くが、すべてを任せられるわけではない。データ型のフィールドに ! を付ける、foldl' を使う、集計の中間値を正格にする、必要なら StrictData を検討する、といった判断が必要になる。正格にしすぎると無限構造や短絡評価の良さを失うため、プロファイル結果を見ながら局所的に変える。
91. RULESプラグマ
{-# RULES
"map/map" forall f g xs. map f (map g xs) = map (f . g) xs
#-}
「コンパイラへの最適化指示」。書き換えルールを定義できる。Haskellの独自機能。
RULES はライブラリ作者向けの機能であり、通常のアプリケーションコードで多用するものではない。うまく使うと、中間データ構造を消したり、連続した変換を一つに畳み込んだりできる。map/map のようなルールは、読みやすい高階関数の形を保ちながら、実行時にはより直接的な処理へ変換するために使われる。
注意点は、ルールが常に発火するとは限らないこと。インライン展開のタイミング、最適化レベル、関数の可視性、型の具体性によって結果が変わる。-ddump-rule-firings などで実際に発火したかを確認し、ベンチマークで効果を見る必要がある。意味を変えるルールを書いてしまうと非常に見つけにくいバグになるため、等式として正しい変換だけを書く。
92. ファクトリ関数の活用
-- data型の構築をスマートにする
data Email = Email String
mkEmail :: String -> Maybe Email
mkEmail s
| '@' `elem` s = Just (Email s)
| otherwise = Nothing
-- export時にEmailコンストラクタは隠す
module Email (Email, mkEmail, getEmail) where ...
-- これでEmailは必ずvalid
「スマートコンストラクタ」パターン。
93. プログラム例集
93-1. ファイル単語数カウント
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
wordCount :: T.Text -> Map.Map T.Text Int
wordCount = foldr (\w -> Map.insertWith (+) w 1) Map.empty . T.words
main = do
text <- TIO.readFile "input.txt"
let counts = wordCount text
mapM_ (\(w, c) -> TIO.putStrLn (w <> ": " <> T.pack (show c))) (Map.toList counts)
93-2. fizzbuzz
fizzbuzz :: Int -> String
fizzbuzz n
| n `mod` 15 == 0 = "FizzBuzz"
| n `mod` 3 == 0 = "Fizz"
| n `mod` 5 == 0 = "Buzz"
| otherwise = show n
main = mapM_ (putStrLn . fizzbuzz) [1..100]
93-3. 素数(無限リスト)
primes :: [Int]
primes = sieve [2..]
where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
main = print (take 100 primes)
「無限リストから先頭100個」が自然に書けるHaskellの美しさ。
93-4. このセクションのまとめ
- Haskellは短いコードで美しい
- 数学的にきれい
- バグが少ない
- 並行も安全
94. Haskellプログラマの哲学
1. 「動くまで時間がかかるが、動いたら正しい」
2. 「型が嘘をつかない」
3. 「関数は数学の式」
4. 「副作用は隔離する」
5. 「並行は値を共有しない」
6. 「遅延は無限を可能にする」
7. 「コードは人間が読むもの、たまに機械も実行する」
この哲学は美しいが、現場では折り合いも必要になる。型で表現できることを増やすほど、不正な状態を作りにくくなる。一方で、型レベルの工夫が読み手の理解を超えると、保守性は下がる。Haskellでは「型で守る」と「普通に読める」のバランスが設計判断そのものになる。
良いHaskellコードは、純粋な核と不純な外側が分かれている。ドメインの変換、検証、計算は純粋関数として表し、外部入力、DB、ネットワーク、時刻、乱数は境界に寄せる。この構造にできると、テストは軽くなり、並行処理も安全になり、エラー処理も型で追いやすくなる。
学び続けるうえでは、抽象を急がないことも大切である。Functor, Applicative, Monad, Traversable, Lens, Effect Systemは、それぞれ具体的な重複や不便さを解くためにある。実例で困ってから抽象へ戻ると、概念が記号ではなく道具として理解できる。
補遺: Haskellの設計思想
96. Haskellが他言語に与えた影響
Haskellが広めた / 発明した概念:
1. 型クラス → Rustのtrait、Scalaのtypeclass
2. モナド → JSのPromise、RustのResultの連鎖
3. 純粋関数型 → Reactの関数型コンポーネント
4. 型推論 → Java/Kotlin/Swiftのvar
5. ADT → Swift/Kotlin/Rustのenum
6. パターンマッチング → 多くの現代言語
7. lazy評価 → Stream API(Java、Python)
8. QuickCheck → Hypothesis (Python)、PropEr
9. functor/applicative → JS Promise.all、Rx
10. STM → Clojure、Java
「Haskellは実用言語ではないが、すべての言語に影響を与えた」と言われる所以。
97. Haskellライフタイム
1990 Haskell 1.0
1998 Haskell 98(標準化)
2010 Haskell 2010
GHCが事実上の進化の場
近年:
- Linear Types(Rust風所有権)
- Dependent Types(型レベル計算)
- polysemy / fused-effects(effect system)
- Cabal / Stackの改善
未来:
- LLM時代の関数型
- Cardanoの成熟
- 教育での復活
Haskell エコシステム と 学習リソース
Haskell.org の公式リソース
Haskell.org は言語仕様、コンパイラ、パッケージマネージャの中心地。
主要コンポーネント
| コンポーネント | 役割 |
|---|---|
| GHC (Glasgow Haskell Compiler) | 最も使われるHaskellコンパイラ |
| Cabal | パッケージビルドシステム(C++ の CMake に相当) |
| Stack | GHC+Cabalのラッパー(依存管理を簡略化) |
| Hackage | Haskell パッケージリポジトリ |
Cabal と Stack の選択
Cabal - より古く、細かい制御が可能。大規模プロジェクト向け。
cabal init
cabal build
cabal run
Stack - より新しく、シンプル。初心者・中規模プロジェクト向け。
stack new myproject
stack build
stack run
Stack は Haskell Tool Stack の略で、GHC バージョン管理も自動化。
Hackage と Haddock ドキュメント
Hackage は PyPI (Python) や npm (JavaScript) に相当するパッケージレジストリ。
各パッケージには Haddock で生成されたHTMLドキュメントが含まれます。
- 関数の型署名
- サンプルコード
- 計算量の注釈
型クラスの標準ライブラリ
Prelude - 最小限の標準ライブラリ(Eq, Ord, Show, Num など)
Data モジュール - コンテナとデータ構造
import Data.List -- list utilities
import Data.Map -- key-value store
import Data.Set -- set operations
import Data.Vector -- mutable/immutable vectors
Control モジュール - 制御フロー
import Control.Monad -- monad utilities (>>=, return)
import Control.Applicative -- applicative functors (<*>, <{{CONTENT}}gt;)
Haskell による関数型プログラミングの実践
Functor, Applicative, Monad の階層
Haskell の型クラス階層:
Functor (map/fmap)
↓
Applicative (<*>, pure)
↓
Monad (>>=, return)
Functor: 値をコンテキスト内で変換
fmap (+1) [1, 2, 3] -- [2, 3, 4]
fmap (+1) (Just 5) -- Just 6
Applicative: コンテキスト内で関数を適用
(+) <{{CONTENT}}gt; [1, 2] <*> [10, 20] -- [11, 21, 12, 22]
Monad: 計算を順序立てて実行(副作用を管理)
do
x <- readLine
y <- readLine
return (x ++ y)
Maybe と Either による エラーハンドリング
Maybe a - 値があるか、ないか
case safeDivide 10 2 of
Just result -> print result
Nothing -> print "Division by zero"
Either e a - 値があるか、エラーがあるか
safeDivide :: Double -> Double -> Either String Double
safeDivide x 0 = Left "Division by zero"
safeDivide x y = Right (x / y)
List Comprehensions と Lazy Evaluation
リスト内包表記:
[x * 2 | x <- [1..10], x `mod` 2 == 0] -- [4, 8, 12, ...]
遅延評価により、無限リストも表現可能:
take 5 [1..] -- 最初の5要素のみ計算: [1, 2, 3, 4, 5]
パターンマッチングの力
-- リストのパターンマッチ
sumList [] = 0
sumList (x:xs) = x + sumList xs
-- タプルのパターンマッチ
swap (a, b) = (b, a)
-- ガード
maxOf a b
| a >= b = a
| otherwise = b
Haskell パフォーマンスチューニング
Lazy Evaluation のコスト
遅延評価は柔軟ですが、メモリ効率が悪いことがあります。
Strict Evaluation を明示的に指定:
{-# LANGUAGE BangPatterns #-}
sumStrict :: [Int] -> Int
sumStrict xs = go 0 xs
where
go !acc [] = acc
go !acc (x:xs) = go (acc + x) xs
バナナ記号 ! は「この値を即座に評価せよ」を意味します。
基本パッケージと Data.Vector
リストは遅延評価だが、Data.Vector は強制評価(より高速):
import Data.Vector qualified as V
-- リスト
xs = [1..1000000]
-- ベクトル
v = V.fromList [1..1000000]
ベクトル上の操作は配列的で、ランダムアクセスがO(1)。
Profile とボトルネック検出
ghc -prof -fprof-auto MyProgram.hs
./MyProgram +RTS -p
.prof ファイルに関数ごとの実行時間とメモリ使用量が記録される。
Haskell によるWeb開発
Yesod フレームワーク
高型安全なWEBフレームワーク:
{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
import Yesod
data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
/user/#String UserR GET
|]
instance Yesod App
getHomeR = return $ object ["message" .= ("Hello, Haskell!" :: String)]
getUserR name = return $ object ["user" .= name]
main = warp 3000 App
RESTful API と JSON
JSON 生成・パース:
import Data.Aeson
import qualified Data.ByteString.Lazy as BL
data User = User { name :: String, age :: Int }
deriving (Generic, ToJSON, FromJSON)
encodeUser u = BL.putStr $ encode u
Database 操作(Persistent)
型安全なデータベースアクセス:
{-# LANGUAGE TemplateHaskell, QuasiQuotes, EmptyDataDecls #-}
import Database.Persist
import Database.Persist.Sqlite
mkPersist sqlSettings [persistLowerCase|
User
name String
email String
deriving Show
|]
main = runSqlite "test.db" $ do
userId <- insert $ User "Alice" "alice@example.com"
user <- get userId
print user
Haskell 学習の次のステップ
Haskell を習得することで:
- 関数型思考: 副作用を明示的に扱う
- 型システムの力: Hindley-Milner 型推論と依存型(Agda/Idris)への架け橋
- 遅延評価の理解: Python ジェネレータ、JavaScript Promise の深い理解に繋がる
- 並行・並列プログラミング:
forkIO,STM(Software Transactional Memory) で lock-free 並行を実装
Haskell コミュニティは、学術・産業双方で活動中。金融(Janestreet)、Web(Hasura)、フォーマルベリフィケーション(Cardano)などの領域で採用例が増加しています。
まとめ
Haskellは、純粋関数、遅延評価、強い静的型、型クラスを通じて、プログラムを数学的な構造として扱いやすくする言語です。実務で使う場合も、型で不変条件を表し、副作用を境界に押し出す考え方は、多くの言語に応用できます。