第3章: 純粋関数による至福
再び純粋であるために
まず明確にすべきは、純粋関数(Pure Function)の概念です。
純粋関数とは、同じ入力を受け取った場合、常に同じ出力を返し、観測可能な副作用(Side Effect)を持たない関数を指します。
例えばsliceとsplice。これらは同じ処理を全く異なる方法で実装していますが、前者sliceは純粋と言えます。なぜなら毎回同じ入力に対して常に同じ出力を保証するからです。一方spliceは配列を恒久的に変更して状態変化を引き起こすため、観測可能な副作用を伴います。
const xs = [1,2,3,4,5];
// pure
xs.slice(0,3); // [1,2,3]
xs.slice(0,3); // [1,2,3]
xs.slice(0,3); // [1,2,3]
// impure
xs.splice(0,3); // [1,2,3]
xs.splice(0,3); // [4,5]
xs.splice(0,3); // []関数型プログラミングでは、spliceのようなデータを改変する扱いにくい関数を好みません。毎回同じ結果を返す信頼性のある関数を目指すからです。
別の例を見てみましょう。
// impure
let minimum = 21;
const checkAge = age => age >= minimum;
// pure
const checkAge = (age) => {
const minimum = 21;
return age >= minimum;
};非純粋なcheckAgeは外部状態である変数minimumに依存しています。これは外部環境を参照することで認知負荷を増加させる要因となります。
一見些細な問題のようですが、状態への依存はシステムの複雑化を引き起こす主な原因の一つです(http://curtclifton.net/papers/MoseleyMarks06a.pdf)。このような関数は入力以外の要因で結果が変化する可能性があり、純粋性を失わせるだけでなく、ソフトウェアの推論を困難にします。
一方、純粋な実装は完全に自己完結しています。immutable(不変)なminimumを設定することで状態変更を防ぎ、純粋性を維持できます。
const immutableState = Object.freeze({ minimum: 21 });副作用がもたらすもの...
副作用(Side Effect)の本質を深掘りしましょう。純粋関数の定義で言及されるこの忌むべき副作用とは一体何でしょうか?ここでの「効果(Effect)」とは、計算結果の導出以外で発生するすべての作用を指します。
効果自体に問題があるわけではありません。問題は「副作用」の「側面」にあります。水自体は幼虫の発生源ではありませんが、淀んだ水が発生源になるように、副作用はプログラム内で想定外の事態を引き起こす温床となるのです。
副作用とは、計算過程におけるシステム状態の変更、または外部世界との観測可能な相互作用を指します。
副作用の例を以下に示します:
- ファイルシステムの変更
- データベースへのレコード挿入
- HTTPリクエストの送信
- データのミューテーション(改変)
- 画面表示/ログ出力
- ユーザー入力の取得
- DOMクエリ
- システム状態へのアクセス
このように、関数外部との相互作用は全て副作用となります。純粋関数だけでプログラムを構築する現実性に疑問を抱くかもしれませんが、関数型プログラミングの哲学は、副作用が不正な挙動の主要因であると位置付けています。
副作用の使用を完全に禁止するのではなく、制御された形で管理することが重要です。後の章で学ぶファンクター(Functor)やモナド(Monad)でその方法を習得しますが、現段階では純粋関数を非純粋な関数から分離することを心掛けましょう。
副作用が存在すると、その関数は純粋性を失います。定義の通り、純粋関数は同じ入力に対して常に同じ出力を保証する必要があるためです。
なぜ入出力の関係を厳密に要求するのか、数学的視点から考察してみましょう。中学校の数学の時間です。
中学校数学
mathisfun.comより:
関数とは、入力値と出力値の間に成り立つ特殊な関係です。各入力値は正しく1つの出力値を返します。
つまり、入力と出力の単なる関連性を指します。各入力が1つの出力を持つ必要がありますが、異なる入力が同じ出力を持つ可能性はあります。
(https://www.mathsisfun.com/sets/function.html)
対照的に、次に示す図形は関数とは言えません。入力値5が複数の出力値に対応しているためです:
(https://www.mathsisfun.com/sets/function.html)
関数は(入力, 出力)のペアで表現可能です:[(1,2), (3,6), (5,10)](この関数は入力を2倍しています)
表形式でも表現可能:
| Input | Output |
|---|---|
| 1 | 2 |
| 2 | 4 |
| 3 | 6 |
また、x軸を入力、y軸を出力とするグラフでも表現できます:

入力によって出力が決定される限り、実装詳細は不要です。オブジェクトリテラルを定義し、()の代わりに[]で実行することも可能です。
const toLowerCase = {
A: 'a',
B: 'b',
C: 'c',
D: 'd',
E: 'e',
F: 'f',
};
toLowerCase['C']; // 'c'
const isPrime = {
1: false,
2: true,
3: true,
4: false,
5: true,
6: false,
};
isPrime[3]; // trueもちろん実用的には計算処理が必要ですが、これが関数に対する異なる視点を提供します。(複数引数の扱いは数学的には面倒ですが、配列でまとめるかカリー化(Currying)の技術で解決できます)
劇的な結論を明かしましょう:純粋関数は数学的関数そのものであり、関数型プログラミングの中核です。これらの性質がもたらす多大な利点を確認しましょう。
純粋性の意義
キャッシュ可能
純粋関数は入力値で常にキャッシュ可能です。この技術をメモ化(Memoization)と呼びます:
const squareNumber = memoize(x => x * x);
squareNumber(4); // 16
squareNumber(4); // 16, returns cache for input 4
squareNumber(5); // 25
squareNumber(5); // 25, returns cache for input 5シンプルな実装例(より堅牢な実装も存在します)
const memoize = (f) => {
const cache = {};
return (...args) => {
const argStr = JSON.stringify(args);
cache[argStr] = cache[argStr] || f(...args);
return cache[argStr];
};
};非純粋関数を評価遅延により純粋化する例:
const pureHttpCall = memoize((url, params) => () => $.getJSON(url, params));実際のHTTPリクエストは送信せず、代わりに指定されたurlとparamsでリクエストを送信する関数を返します。これは純粋関数です。
memoize関数は生成された関数自体をキャッシュします。
現状では有用性が低いですが、後に有用なテクニックを学びます。重要なのは、どんな関数でもキャッシュ可能という点です。
移植性/自己完結性
純粋関数は完全に自己完結しています。明示的な依存関係を持つため、内部の挙動が把握しやすい特徴があります。
// impure
const signUp = (attrs) => {
const user = saveUser(attrs);
welcomeUser(user);
};
// pure
const signUp = (Db, Email, attrs) => () => {
const user = saveUser(Db, attrs);
welcomeUser(Email, user);
};純粋関数のシグネチャを見るだけで、Db、Email、attrsを使用することが少なくとも明示的です。
純粋形式は不透明な非純粋関数よりも多くの情報を提供します。
依存関係を「注入」することで、アプリの柔軟性が向上します。新しい環境でも信頼性のある関数を再利用可能です。
JavaScript環境では、関数のシリアライズ/ソケット送信、Web Workerでの実行も可能です。
状態や依存関係に縛られる手続き型プログラミングと異なり、純粋関数はどこでも実行可能です。
Erlang生みの親Joe Armstrongの名言:「オブジェクト指向言語の問題点は、暗黙の環境を抱え込むこと。欲しかったのはバナナなのに、ゴリラとジャングルごと掴まされる」(原文:'You wanted a banana but what you got was a gorilla holding the banana... and the entire jungle')
テスト容易性
純粋関数はテストを容易にします。外部ゲートウェイのモックや状態の管理が不要で、入力/出力の検証のみ行えます。
関数型コミュニティでは自動生成入力によるテストツール(例:QuickCheck)が開発されています。
論理的整合性
参照透明性(Referential Transparency)は純粋関数最大の利点です。コードをその評価値に置き換えてもプログラムの挙動が変わらない特性です。
等式推論(Equational Reasoning)を用いて、コードを等価交換的に分析できます。
const { Map } = require('immutable');
// Aliases: p = player, a = attacker, t = target
const jobe = Map({ name: 'Jobe', hp: 20, team: 'red' });
const michael = Map({ name: 'Michael', hp: 20, team: 'green' });
const decrementHP = p => p.set('hp', p.get('hp') - 1);
const isSameTeam = (p1, p2) => p1.get('team') === p2.get('team');
const punch = (a, t) => (isSameTeam(a, t) ? t : decrementHP(t));
punch(jobe, michael); // Map({name:'Michael', hp:19, team: 'green'})pureな関数decrementHP、isSameTeam、punchでコードの置換実験:
まずisSameTeamをインライン展開
const punch = (a, t) => (a.get('team') === t.get('team') ? t : decrementHP(t));immutableデータなので実際のチーム値に置換
const punch = (a, t) => ('red' === 'green' ? t : decrementHP(t));条件分岐がfalseの場合、該当部分を削除
const punch = (a, t) => decrementHP(t);decrementHPを展開するとhpを1減らす呼び出しに簡略化
const punch = (a, t) => t.set('hp', t.get('hp') - 1);この推論能力はリファクタリングとコード理解の強力な武器となります。
並列処理
純粋関数は並列実行可能です。共有メモリへのアクセス不要で、副作用による競合状態が発生しません。
サーバーサイドのスレッド環境やブラウザのWeb Workerでも可能ですが、非純粋関数の複雑さから現状では控えられがちです。
まとめ
純粋関数の意義と関数型プログラマーが重要視する理由を解説しました。今後は原則として純粋関数を志向し、非純粋部分を分離する方針を取ります。
純粋関数のみでプログラムを作成するには追加のツールが必要です。次章ではカリー化(Currying)の技術を習得しましょう。