Haskell

目次

主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。

概要

まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。

flowchart TD A["Haskell"] --> B["純粋関数"] A --> C["強い静的型"] A --> D["遅延評価"] B --> E["副作用を分離"] C --> F["型で設計を表す"] D --> G["必要になるまで計算しない"] E --> H["IOモナド"] F --> I["安全な抽象"] G --> I H --> I
コード例の読み方

コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。

要点

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. 純粋関数型: 副作用を型で表現
  2. 遅延評価: 必要になるまで計算しない
  3. 強い静的型: 型推論 + 型クラス
  4. 数学的厳密さ: ラムダ計算ベース

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

GHCupGHCCabalStack・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

| で条件分岐。otherwiseTrue と同じ。


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

Maybenullの代わり。「あるかも、ないかも」を型で表現。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. モナドとは

計算の文脈を扱うインターフェース」。IOMaybeEitherListState などすべてモナド。

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]

データ並列複数CPUを使い切る


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 も合わせて覚えるとよい。キューを使えば、ワーカー間の仕事の受け渡しを自然に表現できる。retryorElse は強力だが、待ち条件が複雑になりすぎると読み手が追えなくなるため、ドメイン名のついた小さな関数に分ける。


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. IntInteger の違い

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: 実践

  • StackまたはCabalでプロジェクト
  • HUnit / QuickCheck / Hspec
  • 簡単なWebサーバ(scotty)
  • “Learn You a Haskell” 読了

29. 用語集

あ行

  • アトム: 最小単位(式)
  • 遅延評価: 必要になるまで評価しない
  • イミュータブル: 変更不可

か行

  • カリー化(currying): f(a,b) をf(a)(b) に
  • 関数合成: f . g
  • クロージャ: 環境を捕捉する関数

さ行

  • 参照透明性: 式 = 値、文脈に依存しない
  • 正格評価: 即座に評価
  • 総称関数: ジェネリクス

た行

  • 多相型: 型変数を持つ型
  • 代数的データ型(ADT): data … = … | …
  • 直積型 / 直和型: AND / ORの型

な行

は行

  • パーサコンビネータ: パーサを組み合わせる関数群
  • パターンマッチ: 型を分解
  • 不変: イミュータブル

ま行

  • モナド: 文脈付き計算
  • モナドトランスフォーマー: モナドを重ねる

や〜わ行

A〜Z


発展: 型と関数型設計

ここからは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: 実践

  • Cabal / Stackでプロジェクト
  • HSpec / QuickCheck
  • HTTP(http-client)
  • JSON(aeson)
  • 簡単なWebサーバ

Week 5: 上級

  • モナドトランスフォーマー
  • lens
  • 並行処理(STM、async)
  • パフォーマンス(Criterion)

Week 6: 応用


実践: ランタイムとコミュニティ


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

コミュニティ

  • 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


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

型レベルでASTを表現」。型安全なインタプリタが書ける。


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解析

GHCstrictness 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ドキュメントが含まれます。

例:Data.List

  • 関数の型署名
  • サンプルコード
  • 計算量の注釈

型クラスの標準ライブラリ

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 を習得することで:

  1. 関数型思考: 副作用を明示的に扱う
  2. 型システムの力: Hindley-Milner 型推論と依存型(Agda/Idris)への架け橋
  3. 遅延評価の理解: Python ジェネレータ、JavaScript Promise の深い理解に繋がる
  4. 並行・並列プログラミング: forkIO, STM (Software Transactional Memory) で lock-free 並行を実装

Haskell コミュニティは、学術・産業双方で活動中。金融(Janestreet)、Web(Hasura)、フォーマルベリフィケーション(Cardano)などの領域で採用例が増加しています。

まとめ

Haskellは、純粋関数、遅延評価、強い静的型、型クラスを通じて、プログラムを数学的な構造として扱いやすくする言語です。実務で使う場合も、型で不変条件を表し、副作用を境界に押し出す考え方は、多くの言語に応用できます。

参考文献

講義・記事

書籍

解説・補助