パターンマッチ
パターンマッチは「データの構造に基づいて分岐する」強力な制御構造。switch文の進化版であり、関数型プログラミングの中心的な機能。
91 分で読めます45,494 文字
パターンマッチ
パターンマッチは「データの構造に基づいて分岐する」強力な制御構造。switch文の進化版であり、関数型プログラミングの中心的な機能。
この章で学ぶこと
- パターンマッチの種類と表現力を理解する
- 網羅性チェックの重要性を理解する
- 各言語のパターンマッチ機能を比較できる
- 実務でのパターンマッチ活用パターンを習得する
- パターンマッチのアンチパターンを回避できる
- ADT(代数的データ型)との組み合わせを理解する
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- 分岐とループ の内容を理解していること
1. パターンマッチの基本概念
1.1 パターンマッチとは何か
パターンマッチ = 値の「構造」を調べて、合致するパターンに応じた処理を実行する仕組み
switch文との違い:
switch: 値の「等値比較」のみ
match: 値の「構造分解」+ 「条件」+ 「束縛」が可能
パターンマッチの構成要素:
1. リテラルパターン — 具体的な値との一致
2. 変数パターン — 任意の値を束縛
3. ワイルドカードパターン — 任意の値にマッチ(束縛なし)
4. 構造体パターン — データ構造の分解
5. タプルパターン — タプルの分解
6. 列挙型パターン — バリアントの分解
7. ガードパターン — 追加条件の指定
8. OR パターン — 複数パターンの論理和
9. 範囲パターン — 値の範囲指定
10. 束縛パターン — マッチした値に名前を付ける
1.2 Rust のパターンマッチ(最も完成度が高い)
// Rust: match による構造的パターンマッチ
// ========================================
// リテラルパターン
// ========================================
match x {
1 => println!("one"),
2 | 3 => println!("two or three"), // OR パターン
4..=9 => println!("four to nine"), // 範囲パターン
_ => println!("other"), // ワイルドカード
}
// ========================================
// 構造体の分解(Destructuring)
// ========================================
struct Point { x: i32, y: i32 }
match point {
Point { x: 0, y: 0 } => println!("origin"),
Point { x, y: 0 } => println!("on x-axis at {}", x),
Point { x: 0, y } => println!("on y-axis at {}", y),
Point { x, y } if x == y => println!("on diagonal at {}", x),
Point { x, y } => println!("({}, {})", x, y),
}
// フィールドの部分マッチ(残りを無視)
struct Config {
host: String,
port: u16,
debug: bool,
timeout: u64,
}
match config {
Config { debug: true, .. } => println!("Debug mode enabled"),
Config { port: 443, host, .. } => println!("HTTPS on {}", host),
Config { port: 80, host, .. } => println!("HTTP on {}", host),
_ => println!("Custom config"),
}
// ========================================
// 列挙型の分解
// ========================================
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
Color(u8, u8, u8),
}
match msg {
Message::Quit => println!("quit"),
Message::Move { x, y } => println!("move to ({}, {})", x, y),
Message::Write(text) => println!("text: {}", text),
Message::Color(r, g, b) => println!("color: #{:02x}{:02x}{:02x}", r, g, b),
}
// ========================================
// ネストしたパターン
// ========================================
match value {
Some(Some(x)) if x > 0 => println!("positive: {}", x),
Some(Some(x)) => println!("non-positive: {}", x),
Some(None) => println!("inner none"),
None => println!("outer none"),
}
// ネストした列挙型
enum Expr {
Num(f64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
}
fn eval(expr: &Expr) -> f64 {
match expr {
Expr::Num(n) => *n,
Expr::Add(a, b) => eval(a) + eval(b),
Expr::Mul(a, b) => eval(a) * eval(b),
Expr::Neg(e) => -eval(e),
}
}
// ========================================
// ガード条件(match guard)
// ========================================
match num {
n if n < 0 => println!("negative"),
n if n == 0 => println!("zero"),
n if n % 2 == 0 => println!("positive even: {}", n),
n => println!("positive odd: {}", n),
}
// 外部変数を参照するガード
let threshold = 100;
match value {
v if v > threshold => println!("above threshold"),
v if v == threshold => println!("at threshold"),
v => println!("below threshold: {}", v),
}
// ========================================
// 束縛(@ パターン)— 値にマッチしつつ名前を付ける
// ========================================
match age {
n @ 0..=12 => println!("child: {}", n),
n @ 13..=17 => println!("teen: {}", n),
n @ 18..=64 => println!("adult: {}", n),
n @ 65.. => println!("senior: {}", n),
_ => unreachable!(),
}
// 列挙型の中身に名前を付ける
match msg {
Message::Write(ref text @ _) if text.len() > 100 => {
println!("Long message: {}...", &text[..100]);
}
Message::Write(text) => println!("Message: {}", text),
_ => {}
}
// ========================================
// スライスパターン
// ========================================
match slice {
[] => println!("empty"),
[x] => println!("single: {}", x),
[x, y] => println!("pair: {}, {}", x, y),
[first, .., last] => println!("first={}, last={}", first, last),
}
// スライスパターンの実務的な使用例
fn parse_command(parts: &[&str]) -> Command {
match parts {
["help"] => Command::Help,
["quit" | "exit"] => Command::Quit,
["get", key] => Command::Get(key.to_string()),
["set", key, value] => Command::Set(key.to_string(), value.to_string()),
["del", keys @ ..] => Command::Delete(keys.iter().map(|s| s.to_string()).collect()),
_ => Command::Unknown,
}
}
// ========================================
// 参照パターン
// ========================================
let reference = &42;
match reference {
&val => println!("Got a value: {}", val),
}
// ref キーワード(借用を作る)
let value = String::from("hello");
match value {
ref s => println!("Borrowed: {}", s),
// value はまだ使える
}
// ref mut キーワード
let mut value = vec![1, 2, 3];
match value {
ref mut v => v.push(4),
}1.3 パターンマッチが使える場所(Rust)
// match 式以外でもパターンマッチが使える
// 1. let 束縛
let (x, y, z) = (1, 2, 3);
let Point { x, y } = point;
let Some(value) = optional else { return; }; // let-else (Rust 1.65+)
// 2. if let(単一パターンの簡潔なマッチ)
if let Some(value) = optional {
println!("Got: {}", value);
}
// 3. while let
while let Some(item) = stack.pop() {
process(item);
}
// 4. for ループ
for (index, value) in collection.iter().enumerate() {
println!("{}: {}", index, value);
}
// 5. 関数の引数
fn print_point(&Point { x, y }: &Point) {
println!("({}, {})", x, y);
}
// 6. クロージャの引数
let points: Vec<Point> = get_points();
let sum_x: i32 = points.iter()
.map(|&Point { x, .. }| x)
.sum();
// 7. let-else パターン(Rust 1.65+)
fn parse_config(input: &str) -> Result<Config, Error> {
let Some(line) = input.lines().next() else {
return Err(Error::EmptyInput);
};
let [key, value] = line.splitn(2, '=').collect::<Vec<_>>()[..] else {
return Err(Error::InvalidFormat);
};
Ok(Config { key: key.to_string(), value: value.to_string() })
}2. 各言語のパターンマッチ
2.1 Python(3.10+ match文)
# Python 3.10: Structural Pattern Matching(PEP 634)
# ========================================
# リテラルパターンとOR パターン
# ========================================
match command:
case "quit":
sys.exit()
case "hello" | "hi" | "hey":
print("Hello!")
case str(s) if s.startswith("/"):
handle_command(s)
case _:
print("Unknown")
# ========================================
# シーケンスパターン(タプル、リスト)
# ========================================
match point:
case (0, 0):
print("Origin")
case (x, 0):
print(f"X-axis at {x}")
case (0, y):
print(f"Y-axis at {y}")
case (x, y) if x == y:
print(f"On diagonal at ({x}, {y})")
case (x, y):
print(f"({x}, {y})")
# 可変長シーケンスパターン
match sequence:
case []:
print("Empty")
case [x]:
print(f"Single: {x}")
case [x, y]:
print(f"Pair: {x}, {y}")
case [first, *rest]:
print(f"First: {first}, rest: {rest}")
case [first, *middle, last]:
print(f"First: {first}, last: {last}, middle: {middle}")
# ========================================
# マッピングパターン(辞書)
# ========================================
match config:
case {"type": "postgres", "host": host, "port": port}:
connect_postgres(host, port)
case {"type": "sqlite", "path": path}:
connect_sqlite(path)
case {"type": "redis", **rest}:
connect_redis(**rest) # 残りのキーを捕捉
case _:
raise ValueError("Unknown database type")
# ========================================
# クラスパターン
# ========================================
from dataclasses import dataclass
@dataclass
class Click:
position: tuple[int, int]
button: str
@dataclass
class KeyPress:
key: str
modifiers: list[str]
@dataclass
class Scroll:
direction: str
amount: int
match event:
case Click(position=(x, y), button="left") if x > 100:
print(f"Left click at ({x}, {y})")
case Click(position=(x, y), button="right"):
show_context_menu(x, y)
case KeyPress(key="Enter", modifiers=[]):
submit_form()
case KeyPress(key="Enter", modifiers=["Ctrl"]):
new_line()
case KeyPress(key=k, modifiers=mods) if "Ctrl" in mods:
handle_shortcut(k, mods)
case KeyPress(key=k):
type_character(k)
case Scroll(direction="up", amount=n):
scroll_up(n)
case Scroll(direction="down", amount=n):
scroll_down(n)
# ========================================
# 型パターン
# ========================================
match value:
case bool(): # bool は int のサブクラスなので先にチェック
print(f"Boolean: {value}")
case int(n) if n > 0:
print(f"Positive int: {n}")
case int(n):
print(f"Non-positive int: {n}")
case float(f):
print(f"Float: {f}")
case str(s) if len(s) > 100:
print(f"Long string: {s[:100]}...")
case str(s):
print(f"String: {s}")
case list() as lst if len(lst) > 0:
print(f"Non-empty list of {len(lst)} items")
case _:
print(f"Other: {value}")
# ========================================
# ガード条件(if)
# ========================================
match point:
case (x, y) if x > 0 and y > 0:
print("First quadrant")
case (x, y) if x < 0 and y > 0:
print("Second quadrant")
case (x, y) if x < 0 and y < 0:
print("Third quadrant")
case (x, y) if x > 0 and y < 0:
print("Fourth quadrant")
case (x, y):
print("On axis")
# ========================================
# 実務的な例: JSON API レスポンスの処理
# ========================================
def handle_api_response(response: dict) -> str:
match response:
case {"status": "success", "data": {"users": [*users]}}:
return f"Found {len(users)} users"
case {"status": "success", "data": {"user": {"name": name, "email": email}}}:
return f"User: {name} ({email})"
case {"status": "error", "code": 404, "message": msg}:
return f"Not found: {msg}"
case {"status": "error", "code": code, "message": msg} if code >= 500:
log_error(f"Server error {code}: {msg}")
return f"Server error: {msg}"
case {"status": "error", "code": code, "message": msg}:
return f"Error {code}: {msg}"
case _:
return "Unknown response format"
# ========================================
# 実務的な例: AST(抽象構文木)の評価
# ========================================
def evaluate(expr):
match expr:
case {"type": "number", "value": n}:
return n
case {"type": "string", "value": s}:
return s
case {"type": "binary", "op": "+", "left": left, "right": right}:
return evaluate(left) + evaluate(right)
case {"type": "binary", "op": "-", "left": left, "right": right}:
return evaluate(left) - evaluate(right)
case {"type": "binary", "op": "*", "left": left, "right": right}:
return evaluate(left) * evaluate(right)
case {"type": "binary", "op": "/", "left": left, "right": right}:
divisor = evaluate(right)
if divisor == 0:
raise ValueError("Division by zero")
return evaluate(left) / divisor
case {"type": "unary", "op": "-", "operand": operand}:
return -evaluate(operand)
case {"type": "call", "name": name, "args": args}:
evaluated_args = [evaluate(arg) for arg in args]
return call_function(name, evaluated_args)
case _:
raise ValueError(f"Unknown expression: {expr}")2.2 TypeScript(判別ユニオン + switch)
// TypeScript: 判別ユニオンで擬似パターンマッチ
// ========================================
// 基本的な判別ユニオン
// ========================================
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rect":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
// TypeScript は網羅性をチェック(kind の全値を処理しないとエラー)
}
// ========================================
// ts-pattern ライブラリでより強力なパターンマッチ
// ========================================
import { match, P } from 'ts-pattern';
const result = match(shape)
.with({ kind: "circle", radius: P.when(r => r > 10) }, s =>
`Large circle: ${s.radius}`)
.with({ kind: "circle" }, s =>
`Small circle: ${s.radius}`)
.with({ kind: "rect" }, s =>
`Rectangle: ${s.width}x${s.height}`)
.with({ kind: "triangle" }, s =>
`Triangle: base=${s.base}`)
.exhaustive(); // 網羅性チェック
// ========================================
// ts-pattern の高度な使用例
// ========================================
type ApiResponse =
| { status: "loading" }
| { status: "success"; data: unknown }
| { status: "error"; error: string; retryable: boolean };
function renderResponse(response: ApiResponse): string {
return match(response)
.with({ status: "loading" }, () => "Loading...")
.with({ status: "success", data: P.nullish }, () => "No data")
.with({ status: "success", data: P.array() }, (r) =>
`Found ${(r.data as unknown[]).length} items`)
.with({ status: "success" }, (r) => `Data: ${JSON.stringify(r.data)}`)
.with({ status: "error", retryable: true }, (r) =>
`Error: ${r.error} (retryable)`)
.with({ status: "error", retryable: false }, (r) =>
`Fatal error: ${r.error}`)
.exhaustive();
}
// ========================================
// TypeScript: 型ガードとパターンマッチの組み合わせ
// ========================================
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
function processResult<T, E>(result: Result<T, E>): string {
if (result.ok) {
// result.value にアクセス可能(型が絞り込まれる)
return `Success: ${result.value}`;
} else {
// result.error にアクセス可能
return `Error: ${result.error}`;
}
}
// ========================================
// 複雑な判別ユニオンの実務例: Redux アクション
// ========================================
type Action =
| { type: "INCREMENT"; amount: number }
| { type: "DECREMENT"; amount: number }
| { type: "RESET" }
| { type: "SET"; value: number }
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; data: number[] }
| { type: "FETCH_ERROR"; error: string };
interface State {
count: number;
loading: boolean;
error: string | null;
data: number[];
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + action.amount };
case "DECREMENT":
return { ...state, count: state.count - action.amount };
case "RESET":
return { ...state, count: 0 };
case "SET":
return { ...state, count: action.value };
case "FETCH_START":
return { ...state, loading: true, error: null };
case "FETCH_SUCCESS":
return { ...state, loading: false, data: action.data };
case "FETCH_ERROR":
return { ...state, loading: false, error: action.error };
default: {
const _exhaustive: never = action;
return state;
}
}
}
// ========================================
// never 型による網羅性チェックのユーティリティ
// ========================================
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function handleShape(shape: Shape): string {
switch (shape.kind) {
case "circle": return "Circle";
case "rect": return "Rectangle";
case "triangle": return "Triangle";
default: return assertNever(shape);
// 新しい Shape を追加すると、ここでコンパイルエラー
}
}2.3 Haskell(パターンマッチの元祖)
-- Haskell: 関数定義でのパターンマッチ
-- ========================================
-- 基本的な関数パターンマッチ
-- ========================================
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
-- ガード条件
bmiCategory :: Double -> String
bmiCategory bmi
| bmi < 18.5 = "Underweight"
| bmi < 25.0 = "Normal"
| bmi < 30.0 = "Overweight"
| otherwise = "Obese"
-- ========================================
-- case 式
-- ========================================
describe :: [a] -> String
describe xs = case xs of
[] -> "empty"
[_] -> "singleton"
[_,_] -> "pair"
_ -> "many"
-- ========================================
-- リストのパターン(cons パターン)
-- ========================================
head' :: [a] -> a
head' (x:_) = x
head' [] = error "empty list"
-- リストの再帰処理
sum' :: Num a => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
-- リストパターンの応用
zip' :: [a] -> [b] -> [(a, b)]
zip' _ [] = []
zip' [] _ = []
zip' (x:xs) (y:ys) = (x, y) : zip' xs ys
-- ========================================
-- タプルのパターン
-- ========================================
addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
fst3 :: (a, b, c) -> a
fst3 (x, _, _) = x
-- ========================================
-- 代数的データ型のパターンマッチ
-- ========================================
data Shape = Circle Double
| Rectangle Double Double
| Triangle Double Double Double
deriving (Show)
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))
-- ========================================
-- Maybe 型のパターンマッチ
-- ========================================
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)
fromMaybe :: a -> Maybe a -> a
fromMaybe def Nothing = def
fromMaybe _ (Just x) = x
-- ========================================
-- Either 型のパターンマッチ
-- ========================================
data AppError = NotFound String
| Unauthorized
| ValidationError [String]
deriving (Show)
handleError :: Either AppError a -> String
handleError (Left (NotFound resource)) = "Not found: " ++ resource
handleError (Left Unauthorized) = "Unauthorized"
handleError (Left (ValidationError errs)) = "Validation: " ++ unwords errs
handleError (Right _) = "Success"
-- ========================================
-- レコード構文のパターンマッチ
-- ========================================
data User = User
{ userName :: String
, userAge :: Int
, userEmail :: String
} deriving (Show)
greetUser :: User -> String
greetUser User { userName = name, userAge = age }
| age < 18 = "Hi, " ++ name ++ "!"
| otherwise = "Hello, " ++ name ++ "."
-- ========================================
-- as パターン(@)
-- ========================================
firstLetter :: String -> String
firstLetter "" = "Empty"
firstLetter all@(x:_) = "First letter of " ++ all ++ " is " ++ [x]
-- ========================================
-- 二分木のパターンマッチ
-- ========================================
data Tree a = Leaf | Node (Tree a) a (Tree a)
deriving (Show)
-- 木の要素数
treeSize :: Tree a -> Int
treeSize Leaf = 0
treeSize (Node l _ r) = 1 + treeSize l + treeSize r
-- 木の深さ
treeDepth :: Tree a -> Int
treeDepth Leaf = 0
treeDepth (Node l _ r) = 1 + max (treeDepth l) (treeDepth r)
-- 木のフラット化
flatten :: Tree a -> [a]
flatten Leaf = []
flatten (Node l x r) = flatten l ++ [x] ++ flatten r
-- 要素の検索(二分探索木)
search :: Ord a => a -> Tree a -> Bool
search _ Leaf = False
search target (Node left value right)
| target == value = True
| target < value = search target left
| otherwise = search target right
-- 要素の挿入(二分探索木)
insert :: Ord a => a -> Tree a -> Tree a
insert x Leaf = Node Leaf x Leaf
insert x (Node left value right)
| x < value = Node (insert x left) value right
| x > value = Node left value (insert x right)
| otherwise = Node left value right -- 重複は無視2.4 Scala
// Scala: 最も表現力豊かなパターンマッチの1つ
// ========================================
// 基本的なパターンマッチ
// ========================================
val result = x match {
case 1 => "one"
case n if n > 0 => s"positive: $n"
case _ => "other"
}
// ========================================
// case class の分解
// ========================================
case class Person(name: String, age: Int)
case class Address(city: String, country: String)
person match {
case Person("Alice", _) => "Found Alice"
case Person(name, age) if age >= 18 => s"$name is an adult"
case Person(name, age) => s"$name is $age years old"
}
// ========================================
// sealed trait(代数的データ型)
// ========================================
sealed trait Expr
case class Num(value: Double) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Mul(left: Expr, right: Expr) extends Expr
case class Neg(expr: Expr) extends Expr
case class Var(name: String) extends Expr
def eval(expr: Expr, env: Map[String, Double]): Double = expr match {
case Num(n) => n
case Add(l, r) => eval(l, env) + eval(r, env)
case Mul(l, r) => eval(l, env) * eval(r, env)
case Neg(e) => -eval(e, env)
case Var(name) => env.getOrElse(name, throw new RuntimeException(s"Undefined: $name"))
}
// sealed trait → 網羅性チェック
// ========================================
// 型パターン
// ========================================
def describe(x: Any): String = x match {
case i: Int if i > 0 => s"positive int: $i"
case i: Int => s"non-positive int: $i"
case s: String if s.nonEmpty => s"non-empty string: $s"
case s: String => "empty string"
case _: Boolean => "boolean"
case l: List[_] => s"list of ${l.size} elements"
case _ => "unknown"
}
// ========================================
// 抽出子(Extractor)パターン — unapply メソッド
// ========================================
object Email {
def unapply(s: String): Option[(String, String)] = {
val parts = s.split("@")
if (parts.length == 2) Some((parts(0), parts(1)))
else None
}
}
"user@example.com" match {
case Email(user, domain) => s"User: $user, Domain: $domain"
case _ => "Not an email"
}
// カスタム抽出子
object Even {
def unapply(n: Int): Boolean = n % 2 == 0
}
object Positive {
def unapply(n: Int): Boolean = n > 0
}
42 match {
case n if Even.unapply(n) && Positive.unapply(n) => s"$n is positive and even"
case _ => "other"
}
// ========================================
// パーシャル関数(PartialFunction)
// ========================================
val handler: PartialFunction[Int, String] = {
case 200 => "OK"
case 404 => "Not Found"
case 500 => "Internal Server Error"
}
// isDefinedAt でチェック
handler.isDefinedAt(200) // true
handler.isDefinedAt(302) // false
// collect で安全に適用
val codes = List(200, 301, 404, 500)
val messages = codes.collect(handler) // List("OK", "Not Found", "Internal Server Error")
// ========================================
// for 内包表記でのパターンマッチ
// ========================================
val pairs = List((1, "one"), (2, "two"), (3, "three"))
for {
(num, name) <- pairs
if num > 1
} yield s"$num = $name"
// List("2 = two", "3 = three")
// Option のパターンマッチ in for
val users = Map("alice" -> 30, "bob" -> 25)
val emails = Map("alice" -> "alice@example.com")
for {
(name, age) <- users
email <- emails.get(name)
} yield s"$name ($age): $email"
// List("alice (30): alice@example.com")2.5 Elixir
# Elixir: パターンマッチが言語の核心
# ========================================
# 基本的なパターンマッチ(= は束縛演算子)
# ========================================
{:ok, result} = {:ok, 42} # result = 42
[head | tail] = [1, 2, 3, 4] # head = 1, tail = [2, 3, 4]
%{name: name} = %{name: "Gaku", age: 30} # name = "Gaku"
# ========================================
# case 式
# ========================================
case File.read("config.txt") do
{:ok, content} ->
IO.puts("Content: #{content}")
{:error, :enoent} ->
IO.puts("File not found")
{:error, reason} ->
IO.puts("Error: #{reason}")
end
# ========================================
# 関数定義でのパターンマッチ(複数の関数ヘッド)
# ========================================
defmodule Math do
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
def fibonacci(0), do: 0
def fibonacci(1), do: 1
def fibonacci(n) when n > 1, do: fibonacci(n - 1) + fibonacci(n - 2)
end
# ========================================
# マップのパターンマッチ
# ========================================
defmodule UserHandler do
def process(%{role: "admin", name: name}) do
IO.puts("Admin: #{name}")
end
def process(%{role: "user", name: name, verified: true}) do
IO.puts("Verified user: #{name}")
end
def process(%{role: "user", name: name}) do
IO.puts("Unverified user: #{name}")
end
def process(_) do
IO.puts("Unknown role")
end
end
# ========================================
# ピン演算子(^)— 再束縛の防止
# ========================================
x = 1
case {1, 2, 3} do
{^x, y, z} -> "x is 1, y=#{y}, z=#{z}" # ^x は x の現在の値(1)にマッチ
_ -> "no match"
end
# ========================================
# with 式(パイプライン的なパターンマッチ)
# ========================================
def create_user(params) do
with {:ok, name} <- validate_name(params["name"]),
{:ok, email} <- validate_email(params["email"]),
{:ok, user} <- insert_user(%{name: name, email: email}) do
{:ok, user}
else
{:error, :invalid_name} -> {:error, "Invalid name"}
{:error, :invalid_email} -> {:error, "Invalid email"}
{:error, reason} -> {:error, "Database error: #{reason}"}
end
end2.6 OCaml / F#
(* OCaml: ML系のパターンマッチ *)
(* 基本的なパターンマッチ *)
let rec factorial = function
| 0 -> 1
| n -> n * factorial (n - 1)
(* リストのパターンマッチ *)
let rec sum = function
| [] -> 0
| x :: xs -> x + sum xs
(* 代数的データ型 *)
type shape =
| Circle of float
| Rectangle of float * float
| Triangle of float * float * float
let area = function
| Circle r -> Float.pi *. r *. r
| Rectangle (w, h) -> w *. h
| Triangle (a, b, c) ->
let s = (a +. b +. c) /. 2.0 in
Float.sqrt (s *. (s -. a) *. (s -. b) *. (s -. c))
(* Option 型 *)
let safe_divide a b =
match b with
| 0.0 -> None
| _ -> Some (a /. b)
let unwrap_or default = function
| None -> default
| Some x -> x
(* ネストしたパターン *)
type expr =
| Num of float
| Add of expr * expr
| Mul of expr * expr
| Neg of expr
let rec eval = function
| Num n -> n
| Add (a, b) -> eval a +. eval b
| Mul (a, b) -> eval a *. eval b
| Neg e -> -.(eval e)
(* as パターン *)
let describe_list = function
| [] -> "empty"
| [_] -> "singleton"
| (_ :: _ :: _ as lst) -> Printf.sprintf "list of %d" (List.length lst)
(* when ガード *)
let categorize n = match n with
| n when n < 0 -> "negative"
| 0 -> "zero"
| n when n mod 2 = 0 -> "positive even"
| _ -> "positive odd"3. 網羅性チェック(Exhaustiveness Checking)
3.1 なぜ網羅性チェックが重要か
網羅性チェック = 「全てのケースが処理されているか」をコンパイル時に検証
なぜ重要か?
1. 新しいバリアントを追加した時、処理漏れをコンパイルエラーで検出
2. 実行時の予期しない動作を防止
3. デッドコード(到達不能なパターン)の検出
4. リファクタリング時の安全性を保証
網羅性チェックの仕組み:
- コンパイラがパターンの集合を分析
- 全ての可能な値がカバーされているか検証
- カバーされていない場合はコンパイルエラー(または警告)
3.2 各言語の網羅性チェック
// Rust: 網羅性チェック(最も厳密)
enum Color { Red, Green, Blue }
fn describe(c: Color) -> &'static str {
match c {
Color::Red => "red",
Color::Green => "green",
// Color::Blue が未処理 → コンパイルエラー:
// error[E0004]: non-exhaustive patterns: `Blue` not covered
}
}
// 新しいバリアント追加時の安全性
enum Color { Red, Green, Blue, Yellow } // Yellow 追加
// → 全ての match 文でコンパイルエラーが発生
// → 処理漏れを確実に検出
// Option 型の網羅性
fn process(opt: Option<i32>) -> String {
match opt {
Some(n) if n > 0 => format!("positive: {}", n),
Some(n) => format!("non-positive: {}", n),
None => "nothing".to_string(),
// 全パターンを網羅 → OK
}
}
// Result 型の網羅性
fn handle(result: Result<String, AppError>) -> String {
match result {
Ok(value) => value,
Err(AppError::NotFound(msg)) => format!("Not found: {}", msg),
Err(AppError::Unauthorized) => "Unauthorized".to_string(),
Err(AppError::Validation(errors)) => format!("Invalid: {:?}", errors),
// 全エラーバリアントを網羅する必要がある
}
}
// 数値型の網羅性(ワイルドカードが必要)
fn categorize(n: i32) -> &'static str {
match n {
0 => "zero",
1..=100 => "small positive",
// i32 の全範囲をカバーする必要がある → _ が必要
_ => "other",
}
}// TypeScript: never 型による網羅性チェック
type Color = "red" | "green" | "blue";
function describe(c: Color): string {
switch (c) {
case "red": return "Red";
case "green": return "Green";
case "blue": return "Blue";
default:
const _exhaustive: never = c;
// "yellow" を追加すると、ここでコンパイルエラー
return _exhaustive;
}
}
// satisfies 演算子での網羅性チェック(TypeScript 4.9+)
type EventType = "click" | "hover" | "scroll";
const handlers = {
click: (e: MouseEvent) => { /* ... */ },
hover: (e: MouseEvent) => { /* ... */ },
scroll: (e: Event) => { /* ... */ },
} satisfies Record<EventType, Function>;
// EventType に新しい値を追加すると、handlers でエラーになる
// マップ型での網羅性チェック
type StatusCode = 200 | 201 | 400 | 404 | 500;
const statusMessages: Record<StatusCode, string> = {
200: "OK",
201: "Created",
400: "Bad Request",
404: "Not Found",
500: "Internal Server Error",
// StatusCode に値を追加すると、ここにも追加が必要
};-- Haskell: コンパイル時の網羅性チェック(-Wall オプション)
data Color = Red | Green | Blue
describe :: Color -> String
describe Red = "red"
describe Green = "green"
-- Blue が未処理
-- GHC: warning: [-Wincomplete-patterns]
-- Pattern match(es) are non-exhaustive
-- In an equation for 'describe': Patterns not matched: Blue// Scala: sealed trait で網羅性チェック
sealed trait Color
case object Red extends Color
case object Green extends Color
case object Blue extends Color
def describe(c: Color): String = c match {
case Red => "red"
case Green => "green"
// Blue が未処理
// warning: match may not be exhaustive
// It would fail on the following input: Blue
}3.3 網羅性チェックがない言語での対策
# Python: match 文に網羅性チェックはない
# → mypy のプラグインで部分的にチェック可能
from enum import Enum
from typing import assert_never
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
def describe(c: Color) -> str:
match c:
case Color.RED:
return "Red"
case Color.GREEN:
return "Green"
case Color.BLUE:
return "Blue"
case _ as unreachable:
assert_never(unreachable) # mypy が網羅性をチェック// Go: 網羅性チェックはないが、exhaustive リンターが利用可能
// go install github.com/nishanths/exhaustive/cmd/exhaustive@latest
type Color int
const (
Red Color = iota
Green
Blue
)
func describe(c Color) string {
switch c {
case Red:
return "red"
case Green:
return "green"
// Blue が未処理 → exhaustive リンターが警告
default:
return "unknown"
}
}4. パターンマッチと代数的データ型(ADT)
4.1 ADT とパターンマッチの相性
代数的データ型(Algebraic Data Types):
直積型(Product Type): 複数のフィールドを持つ(構造体、タプル)
直和型(Sum Type): 複数のバリアントのいずれか(列挙型、ユニオン型)
パターンマッチは直和型と完璧に組み合わさる:
→ 各バリアントに対してパターンを定義
→ 網羅性チェックで安全性を保証
→ データの構造分解で内部の値にアクセス
// Rust: ADT + パターンマッチの実務例
// ========================================
// HTTPレスポンスの型安全な表現
// ========================================
enum HttpResponse {
Ok { body: String, headers: HashMap<String, String> },
Created { id: String, location: String },
NoContent,
BadRequest { errors: Vec<ValidationError> },
NotFound { resource: String },
Unauthorized { reason: String },
InternalError { message: String, trace: Option<String> },
}
fn render_response(response: HttpResponse) -> (u16, String) {
match response {
HttpResponse::Ok { body, .. } => (200, body),
HttpResponse::Created { id, location } => {
(201, format!(r#"{{"id": "{}", "location": "{}"}}"#, id, location))
}
HttpResponse::NoContent => (204, String::new()),
HttpResponse::BadRequest { errors } => {
let msgs: Vec<String> = errors.iter()
.map(|e| format!("{}: {}", e.field, e.message))
.collect();
(400, format!(r#"{{"errors": [{}]}}"#, msgs.join(", ")))
}
HttpResponse::NotFound { resource } => {
(404, format!(r#"{{"error": "{} not found"}}"#, resource))
}
HttpResponse::Unauthorized { reason } => {
(401, format!(r#"{{"error": "{}"}}"#, reason))
}
HttpResponse::InternalError { message, trace } => {
if let Some(t) = trace {
eprintln!("Internal error trace: {}", t);
}
(500, format!(r#"{{"error": "{}"}}"#, message))
}
}
}
// ========================================
// コンパイラの AST 表現
// ========================================
enum Type {
Int,
Float,
Bool,
String,
Array(Box<Type>),
Function { params: Vec<Type>, returns: Box<Type> },
Optional(Box<Type>),
}
fn type_to_string(ty: &Type) -> String {
match ty {
Type::Int => "int".to_string(),
Type::Float => "float".to_string(),
Type::Bool => "bool".to_string(),
Type::String => "string".to_string(),
Type::Array(inner) => format!("{}[]", type_to_string(inner)),
Type::Function { params, returns } => {
let param_strs: Vec<String> = params.iter()
.map(type_to_string)
.collect();
format!("({}) -> {}", param_strs.join(", "), type_to_string(returns))
}
Type::Optional(inner) => format!("{}?", type_to_string(inner)),
}
}4.2 Option / Maybe パターン
// Rust: Option<T> の活用パターン
// map — Some の中身を変換
let length: Option<usize> = name.map(|n| n.len());
// and_then — ネストした Option をフラット化
let first_char: Option<char> = name.and_then(|n| n.chars().next());
// unwrap_or — デフォルト値
let display_name = name.unwrap_or("Anonymous");
// unwrap_or_else — 遅延評価のデフォルト値
let display_name = name.unwrap_or_else(|| generate_default_name());
// ? 演算子(Option の連鎖)
fn get_street_name(user: &User) -> Option<String> {
let address = user.address.as_ref()?;
let street = address.street.as_ref()?;
Some(street.clone())
}
// filter
let even_number: Option<i32> = some_number.filter(|n| n % 2 == 0);
// zip — 2つの Option を結合
let full_name: Option<String> = first_name.zip(last_name)
.map(|(first, last)| format!("{} {}", first, last));
// ok_or — Option を Result に変換
let value: Result<i32, AppError> = optional_value
.ok_or(AppError::NotFound("value".to_string()))?;-- Haskell: Maybe の活用パターン
-- fmap(Functor)
length' :: Maybe String -> Maybe Int
length' = fmap length
-- >>= (Monad bind)
firstChar :: Maybe String -> Maybe Char
firstChar name = name >>= safeHead
where
safeHead [] = Nothing
safeHead (x:_) = Just x
-- do 記法
getStreetName :: User -> Maybe String
getStreetName user = do
address <- userAddress user
street <- addressStreet address
return (streetName street)
-- fromMaybe(デフォルト値)
displayName :: Maybe String -> String
displayName = fromMaybe "Anonymous"5. パターンマッチのアンチパターン
5.1 よくある間違い
❌ ワイルドカードの過剰使用
match color {
Color::Red => "red",
_ => "other", // Green, Blue の個別処理を忘れる可能性
}
// 新しいバリアントが追加されても気づかない
✅ 全バリアントを明示的に処理
match color {
Color::Red => "red",
Color::Green => "green",
Color::Blue => "blue",
}
// 新しいバリアントが追加されるとコンパイルエラー
❌ パターンの順序ミス
match n {
_ => "any", // 全てマッチ → 以下は到達不能
1 => "one", // 到達不能(unreachable pattern)
}
❌ ガード条件の網羅性の罠
match n {
x if x > 0 => "positive",
x if x < 0 => "negative",
// x == 0 のケースが抜けている!
// Rust はガード条件の網羅性を保証できない → _ が必要
}
✅ ガード条件を使う場合は最後にキャッチオール
match n {
x if x > 0 => "positive",
x if x < 0 => "negative",
_ => "zero", // 0 をキャッチ
}
5.2 パフォーマンスの考慮
パターンマッチのコンパイル:
1. 決定木(Decision Tree)に変換される
2. 各パターンを順番にチェックするのではなく、最適化される
3. 一般的に O(1) 〜 O(log n) の計算量
最適化のヒント:
- 頻出パターンを先に配置
- 不必要なガード条件を避ける
- ワイルドカードは最後に配置
Rust のマッチの最適化:
- 整数のマッチ → ジャンプテーブルまたは二分探索
- 列挙型のマッチ → タグの比較(O(1))
- 文字列のマッチ → ハッシュまたは逐次比較
5.3 複雑すぎるパターンの回避
// ❌ 複雑すぎるパターン(読みにくい)
match response {
Ok(Response { status: 200, body: Some(Body { content_type: "json", data, .. }), .. })
if data.len() > 0 => {
parse_json(data)
}
Ok(Response { status: 200, body: Some(Body { content_type: "xml", data, .. }), .. })
if data.len() > 0 => {
parse_xml(data)
}
Ok(Response { status: code @ 200..=299, .. }) => {
handle_success(code)
}
Ok(Response { status: code @ 400..=499, .. }) => {
handle_client_error(code)
}
Err(e) => handle_error(e),
_ => handle_unknown(),
}
// ✅ ヘルパー関数に分解
fn process_response(response: Result<Response, Error>) -> Output {
match response {
Ok(resp) => process_ok_response(resp),
Err(e) => handle_error(e),
}
}
fn process_ok_response(resp: Response) -> Output {
match resp.status {
200 => process_body(resp.body),
code @ 200..=299 => handle_success(code),
code @ 400..=499 => handle_client_error(code),
_ => handle_unknown(),
}
}
fn process_body(body: Option<Body>) -> Output {
match body {
Some(Body { content_type: "json", data, .. }) if !data.is_empty() => {
parse_json(&data)
}
Some(Body { content_type: "xml", data, .. }) if !data.is_empty() => {
parse_xml(&data)
}
Some(_) => Output::Empty,
None => Output::NoBody,
}
}6. 実務でのパターンマッチ活用
6.1 コマンドパーサー
// Rust: CLI コマンドパーサー
enum Command {
Get { key: String },
Set { key: String, value: String, ttl: Option<u64> },
Delete { keys: Vec<String> },
List { pattern: Option<String>, limit: usize },
Help,
Quit,
}
fn parse_command(input: &str) -> Result<Command, ParseError> {
let parts: Vec<&str> = input.trim().split_whitespace().collect();
match parts.as_slice() {
["get", key] => Ok(Command::Get { key: key.to_string() }),
["set", key, value] => Ok(Command::Set {
key: key.to_string(),
value: value.to_string(),
ttl: None,
}),
["set", key, value, "ttl", ttl_str] => {
let ttl = ttl_str.parse::<u64>()
.map_err(|_| ParseError::InvalidTtl)?;
Ok(Command::Set {
key: key.to_string(),
value: value.to_string(),
ttl: Some(ttl),
})
}
["del", keys @ ..] if !keys.is_empty() => Ok(Command::Delete {
keys: keys.iter().map(|s| s.to_string()).collect(),
}),
["list"] => Ok(Command::List { pattern: None, limit: 100 }),
["list", pattern] => Ok(Command::List {
pattern: Some(pattern.to_string()),
limit: 100,
}),
["list", pattern, "limit", n] => {
let limit = n.parse::<usize>()
.map_err(|_| ParseError::InvalidLimit)?;
Ok(Command::List {
pattern: Some(pattern.to_string()),
limit,
})
}
["help" | "?"] => Ok(Command::Help),
["quit" | "exit" | "q"] => Ok(Command::Quit),
[] => Err(ParseError::Empty),
_ => Err(ParseError::Unknown(input.to_string())),
}
}6.2 JSONバリデーション
# Python: JSON レスポンスのバリデーション
def validate_user_data(data: dict) -> list[str]:
errors = []
match data:
case {"name": str(name)} if len(name) < 2:
errors.append("Name too short")
case {"name": str()}:
pass # OK
case {"name": _}:
errors.append("Name must be a string")
case _:
errors.append("Name is required")
match data:
case {"email": str(email)} if "@" not in email:
errors.append("Invalid email format")
case {"email": str()}:
pass # OK
case {"email": _}:
errors.append("Email must be a string")
case _:
errors.append("Email is required")
match data:
case {"age": int(age)} if age < 0 or age > 150:
errors.append("Age must be between 0 and 150")
case {"age": int()}:
pass # OK
case {"age": _}:
errors.append("Age must be an integer")
case _:
pass # age is optional
return errors6.3 イベント処理システム
// TypeScript: イベント駆動アーキテクチャ
import { match } from 'ts-pattern';
type DomainEvent =
| { type: "UserCreated"; userId: string; email: string; timestamp: Date }
| { type: "UserUpdated"; userId: string; changes: Partial<UserProfile>; timestamp: Date }
| { type: "UserDeleted"; userId: string; reason: string; timestamp: Date }
| { type: "OrderPlaced"; orderId: string; userId: string; items: OrderItem[]; timestamp: Date }
| { type: "OrderShipped"; orderId: string; trackingId: string; timestamp: Date }
| { type: "OrderCancelled"; orderId: string; reason: string; timestamp: Date }
| { type: "PaymentReceived"; orderId: string; amount: number; currency: string; timestamp: Date }
| { type: "PaymentFailed"; orderId: string; error: string; timestamp: Date };
async function handleEvent(event: DomainEvent): Promise<void> {
await match(event)
.with({ type: "UserCreated" }, async (e) => {
await sendWelcomeEmail(e.email);
await createUserProfile(e.userId);
await publishToAnalytics("user_signup", { userId: e.userId });
})
.with({ type: "UserDeleted" }, async (e) => {
await anonymizeUserData(e.userId);
await cancelPendingOrders(e.userId);
await publishToAnalytics("user_churn", { userId: e.userId, reason: e.reason });
})
.with({ type: "OrderPlaced" }, async (e) => {
await reserveInventory(e.items);
await notifyWarehouse(e.orderId);
await sendOrderConfirmation(e.userId, e.orderId);
})
.with({ type: "PaymentFailed" }, async (e) => {
await notifyPaymentTeam(e.orderId, e.error);
await schedulePaymentRetry(e.orderId);
})
.otherwise(async (e) => {
console.log(`Unhandled event: ${e.type}`);
});
}FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 言語 | パターンマッチ | 網羅性チェック | 特徴 |
|---|---|---|---|
| Rust | match(式) | コンパイル時 | 最も安全、スライスパターン |
| Haskell | case / 関数定義 | コンパイル時(-Wall) | 元祖、ガード条件 |
| Scala | match(式) | sealed trait で保証 | 抽出子、表現力最大 |
| OCaml/F# | match / function | コンパイル時 | ML系の伝統 |
| Elixir | case / 関数ヘッド | なし | ピン演算子、with 式 |
| Python | match(3.10+) | なし(mypy で部分的) | 構造的パターン |
| TypeScript | switch + never | 型レベル | 判別ユニオン、ts-pattern |
| Java | switch(21+) | sealed class で保証 | パターンマッチング switch |
| Kotlin | when(式) | sealed class で保証 | 範囲、型チェック |
パターンの種類と対応言語
| パターン種類 | Rust | Haskell | Scala | Python | TypeScript |
|---|---|---|---|---|---|
| リテラル | ✅ | ✅ | ✅ | ✅ | ✅ |
| 変数束縛 | ✅ | ✅ | ✅ | ✅ | ✅ |
| ワイルドカード | ✅ | ✅ | ✅ | ✅ | ✅ |
| OR パターン | ✅ | - | ✅ | ✅ | - |
| 範囲 | ✅ | - | - | - | - |
| 構造体分解 | ✅ | ✅ | ✅ | ✅ | ✅(ts-pattern) |
| ネスト | ✅ | ✅ | ✅ | ✅ | ✅(ts-pattern) |
| ガード条件 | ✅ | ✅ | ✅ | ✅ | ✅(ts-pattern) |
| スライス | ✅ | ✅ | - | ✅ | - |
| @ 束縛 | ✅ | ✅ | ✅ | ✅(as) | - |
| 参照 | ✅ | - | - | - | - |
| 抽出子 | - | - | ✅ | - | - |
次に読むべきガイド
参考文献
- "Rust By Example: Pattern matching." doc.rust-lang.org.
- "PEP 634: Structural Pattern Matching." python.org, 2021.
- Klabnik, S. & Nichols, C. "The Rust Programming Language." Ch.18, 2023.
- Lipovaca, M. "Learn You a Haskell for Great Good!" Ch.4, No Starch Press, 2011.
- Odersky, M., Spoon, L. & Venners, B. "Programming in Scala." 5th Ed, Ch.15, Artima, 2021.
- Thomas, D. & Hunt, A. "Programming Elixir." Ch.12, Pragmatic Bookshelf, 2018.
- "TypeScript Handbook: Narrowing." typescriptlang.org.
- "OCaml Manual: Pattern matching." ocaml.org.
- Van Rossum, G. "PEP 636: Structural Pattern Matching: Tutorial." python.org, 2021.
- Jemerov, D. & Isakova, S. "Kotlin in Action." Ch.2, Manning, 2017.