概要
関数型プログラミング(FP)における核、それが**純粋関数(pure function)**である。
しかし「副作用がない関数」と言われても、それが設計にどう効くのかは直感的に理解しづらい。
本稿では、Rustを例にしながら「純粋関数とは何か?」「副作用を除外することの意味は何か?」を、
実用・設計・安全性・テスト性の観点から徹底的に分解・言語化する。
1. 純粋関数とは?
✅ 定義:
同じ入力に対して、常に同じ出力を返し、副作用を持たない関数。
fn add(a: i32, b: i32) -> i32 {
a + b
}
- ✅
add(2, 3)
は常に5
を返す - ✅ グローバル変数、IO、状態への書き込みを行わない
2. 純粋関数 vs 非純粋関数
❌ 非純粋関数(副作用を含む)
use std::fs;
fn save_log(message: &str) {
fs::write("log.txt", message).unwrap();
}
- IOを行っている
- 実行するたびに世界(ファイルシステム)を変化させる
- テスト・並列実行が壊れやすい
✅ 純粋関数
fn format_log(level: &str, msg: &str) -> String {
format!("[{}] {}", level, msg)
}
- 入力 → 出力
- 状態を変えない
- 結果に一切のブレがない
3. 純粋関数の利点
観点 | 説明 |
---|---|
テスト | 状態依存がないため、モック不要・DI不要・安定 |
再利用 | 関数の「使い回し」が容易。文脈に依存しない |
並列性 | 状態を持たないため、スレッドセーフが自動的に担保 |
デバッグ | ロジックが独立しており、問題の局所化が容易 |
4. Rustと純粋関数の相性
- Rustは副作用を強制しないが、明示的な所有権とライフタイムで副作用の管理を強化している
- 可変性(mut)もデフォルトで禁止されており、イミュータブルベースの思考に向いている
fn square(x: i32) -> i32 {
x * x
}
- 値の所有 / 移動 / 借用により、副作用の意図的な切り出しが可能
5. 純粋関数を書くための判断フロー
① グローバル変数・IO・ネットワークアクセスを含んでいないか? → YES → 純粋でない
② 入力だけで出力が決まるか? → NO → 振る舞いが曖昧、純粋性に欠ける
③ 状態を書き換えていないか? → YES → 設計の再考
④ 同じ引数で常に同じ値を返すか? → YES → 純粋
6. よくある誤解と対策
❌ 「純粋関数しか使えないと不便なのでは?」
→ ✅ FPは副作用を否定しない。“制御下に置く”ために純粋関数を中心に据える
❌ 「副作用を取り除くと実用的じゃない」
→ ✅ 副作用を末端に限定し、ロジック本体から隔離する設計が理想
❌ 「副作用が悪い」= NG
→ ✅ 予測できない副作用が問題。予測可能にすればOK
7. 構造のデザイン指針
// 副作用を含む関数(ログ出力)を
fn log_to_console(msg: &str) {
println!("{}", msg);
}
// 純粋関数に変換可能
fn build_log_message(level: &str, content: &str) -> String {
format!("[{}] {}", level, content)
}
- ロジックを分離 → 副作用部分をコントローラ層で限定的に呼ぶ
- テストしやすさ / ロジックの流用性が一気に向上する
結語
純粋関数とは、単に「きれいなコード」の話ではない。
それは**「状態」「副作用」「複雑性」といったプログラミングの根本的困難を制御する戦略的な構造」**である。
- 小さく、明確で、テスト可能な関数
- 設計を閉じることで、拡張・変更に強くなる
- 副作用を“排除”ではなく、“支配”するための技術
関数型設計とは、
“混沌と予測不能を、関数と構造で静かに制御する抽象的技術体系である。”