関数型プログラミング
Functional Programming ・ かんすうがたプログラミング
副作用を抑え、純粋関数の合成としてプログラムを組み立てる様式。実行順に左右されない予測しやすいコードを目指す。
✦ 30秒で試す — 航海同じ関数を同じ引数で呼んだのに、答えが変わる上から順に呼ぶと、正しい値が出るのはいくつ?予想してみる概要
関数型プログラミング(FP: Functional Programming)は、プログラムを「状態を書き換える命令の列」ではなく「値を受け取って値を返す関数の合成」として組み立てる様式です。中心にあるのは、同じ入力には必ず同じ出力を返し、外の状態を一切変えない「純粋関数」という考え方で、データを書き換えずに新しい値を作るイミュータビリティ(不変性)とセットで使われます。
Haskell や Elm のような専用の言語だけの話ではありません。JavaScript の map / filter、React の「UI は状態の関数」という設計、Rust のイテレータ、Java の Stream API — 現代の主流の言語やフレームワークには関数型の考え方が深く浸透しており、意識せずに毎日使っている人がほとんどです。純粋な関数型言語を書かない人にとっても、「どこまでを純粋に保ち、どこで副作用を起こすか」という設計の感覚として効いてくる語彙です。
「副作用」とは、戻り値を返す以外に外の世界へ影響を与えること — 変数の書き換え、ファイルやデータベースへの書き込み、画面への出力、現在時刻の取得などを指します。関数型プログラミングは副作用を「禁止」するのではなく、プログラムの端に押し出して、中心部を予測しやすい純粋関数だけで組み立てることを目指します。
なぜ生まれたか
理論のルーツは、1930年代に Alonzo Church が考えたラムダ計算(計算を関数の定義と適用だけで表す数学の体系)です。1958年、John McCarthy はこの考え方を借りて LISP を作り、関数を値として扱い、再帰でデータを処理する流儀をプログラミングに持ち込みました。1973年に Robin Milner らが作った ML は、関数型の書き方に静的な型推論を組み合わせ、1990年の Haskell は副作用を型で区別する「純粋」な言語として研究の到達点になりました。長いあいだ研究者のものと見られていた流れです。
実務の世界で関数型が再評価されたのは、共有される可変状態こそがバグの温床だと痛感されたからです。複数の関数やモジュールが同じ変数を書き換えるプログラムでは、ある関数の結果が「いつ・誰が・何を書き換えたか」という履歴に依存します。再現しないバグ、順番を入れ替えると壊れる処理、モックだらけになるテスト — その多くは、状態の書き換えがプログラム全体に散らばっていることが原因です。マルチコアの時代に入り、複数のスレッドが同じ状態を同時に触ると競合が起きる、という並行と並列の問題が決定打になりました。
関数型プログラミングの答えは、原因そのものを消すことです。データを書き換えず、関数は入力だけから出力を計算する。そうすればどの関数も単独で理解・テストでき、実行順序や並列化を恐れる必要がなくなります。副作用が必要な処理は境界に集め、「純粋な中心部と薄い副作用の殻」という構造にする — この規律が、規模と並行性が増した現代のソフトウェアで実用的な価値を持つようになりました。
詳細
純粋関数と参照透過性
純粋関数は「同じ引数なら必ず同じ戻り値」「外への影響がない」の2条件を満たす関数です。この性質は参照透過性とも呼ばれ、「関数呼び出しをその結果の値に置き換えても、プログラムの意味が変わらない」ことを意味します。税込み価格(1000) はいつどこで呼んでも 1100 ですが、共有の変数 price を読んで書き換える 税込みにする() は、それまでに誰が何回呼んだかで結果が変わる不純な関数です。
参照透過であれば、結果をキャッシュしても(メモ化)、必要になるまで計算を遅らせても(遅延評価)、並列に計算しても安全です。どれも「いつ・何回・どの順で呼んでも同じ値」だから成り立つ最適化です。純粋関数のテストは「入力を渡して戻り値を確かめる」だけで済み、モックも準備も要りません。
次の部品で確かめてください。上の段は共有の変数 price を書き換える関数、下の段は同じ計算を純粋関数で書いたものです。呼び出しのカードをドラッグして並べ替えると、上の段だけ結果が変わります。「同じ順でもう一度呼ぶ」を押すと、上の段は並びが同じでも結果がずれていきます。
上下とも、1000円の商品の 税込・送料込み・会員価格 を計算しています。上の段は2つ間違っています。カードをドラッグして呼ぶ順番を入れ替えると、直るでしょうか。
高階関数 — filter・map・reduce
関数型のもう1つの柱が、関数を値として扱えることです。関数を引数に取ったり戻り値として返したりする関数を高階関数と呼びます。その代表が、コレクション処理の三点セット filter(条件に合う要素を残す)・map(全要素を変換する)・reduce(畳み込んで1つの値にする)です。
const total = orders
.filter(o => o.paid) // 支払い済みだけ残す
.map(o => o.price * 1.1) // 税込みに変換
.reduce((sum, p) => sum + p, 0); // 合計へ畳み込む
for ループでカウンタと途中結果の変数を書き換えていく書き方と比べると、「何をするか」だけが宣言的に並び、「どう繰り返すか」が消えています。ここで渡している小さな関数が外の変数を使えるのはクロージャの働きで、ループの代わりに自分自身を呼ぶ再帰とともに、関数型のスタイルを支える基礎部品です。上の図の右側は、このコードをそのまま構造にしたものです。
副作用の分離 — 純粋な中心と不純な殻
現実のプログラムは副作用なしには成り立ちません。だからこそ設計の焦点は「副作用をどこに置くか」になります。よく知られる指針が「Functional Core, Imperative Shell」— ビジネスロジックの中心部(何をいくらにするか、どう並べ替えるかといった判断)は純粋関数だけで書き、データベースの読み書きや API の呼び出しといった副作用は外側の薄い殻に集める、という構造です。中心部は入出力の値だけでテストでき、殻は「純粋な中心の結果を外界へ運ぶだけ」の単純な役割に痩せていきます。テストが書きにくい、再現しないバグが多いと感じたら、副作用が中心部に染み込んでいるサインです。
落とし穴とトレードオフ
書き換えない規律には、コストもあります。配列に1要素足すたびに全体をコピーしていたら、大きなデータでは遅くてメモリも食います。そこで関数型の言語やライブラリ(Clojure の標準のコレクション、JavaScript の Immutable.js、Scala の Vector など)は、永続データ構造を使います。新しい版は、変わった部分だけを新しく作り、変わらない部分は古い版と共有する木構造です。古い版もそのまま残るので「書き換えない」約束は守られ、コピーの量は木の高さ程度で済みます。
もう1つの落とし穴は、深い再帰です。ループを再帰で書くと、呼び出しのたびにスタックに1段積まれ、要素数が数万を超えるとスタックオーバーフローで落ちます。関数の最後の動作が自分自身の呼び出しである「末尾呼び出し」なら、段を積まずに今の段を使い回す末尾呼び出し最適化で防げますが、これを言語仕様で保証しているのは Scheme や Haskell、Scala(自分自身を呼ぶ末尾再帰に限る)などに限られます。JavaScript は ES2015 で仕様に入れたものの、実装しているのは Safari の JavaScriptCore だけで、Python と Java には最初からありません。これらの言語では、大きなデータには素直に reduce やループを使うのが安全です。
関数型の書き方は、慣れないうちは読みにくいという代償もあります。高階関数を何重にも組み合わせたコードは、書いた本人以外には追いにくくなりがちです。純粋さは手段であって目的ではありません。「共有された書き換えを減らす」という効き目のある部分から取り入れるのが実務的です。
主流言語への浸透
この10年で、関数型の道具立ては主流言語の標準装備になりました。React は「UI は状態を受け取って画面を返す関数」というモデルを採り、状態管理ライブラリの Redux は「新しい状態 = reducer(現在の状態, アクション)」という純粋関数そのものです。画面の差分更新を安全に自動化する仮想DOMも、描画が状態の純粋な関数であることを前提にしています。Rust のイテレータは map / filter の連鎖をループと同じ速さの機械語に最適化し、所有権の仕組みは可変な共有を型システムで禁じます。純粋な計算は分割して並列に計算しても結果が変わらないため、Java の parallel stream のように並列化もしやすくなります。データが変わったら計算し直すというリアクティビティの考え方も、値の流れを関数の合成で記述する関数型の発想と地続きです。
注意したいのは、関数型とオブジェクト指向は二者択一ではないことです。実務では、ドメインの境界と不変条件はオブジェクトで守り、その内側のデータ変換は純粋関数で書く、という併用が自然な着地点になります。まずは「この関数、純粋にできないか?」と自問する習慣から始めるのが、いちばん実りのある入り口です。
試問
1000円の商品の「税込」「送料込み」「会員価格」を、上の段は共有の変数 price を読んで書き換える関数で、下の段は引数 p だけから値を返す純粋関数で計算しています。カードをドラッグすると呼ぶ順番を入れ替えられ(入れ替えると上の段は price = 1000 から計算し直します)、ボタンで同じ順のまま呼び直せます。
上下とも、1000円の商品の 税込・送料込み・会員価格 を計算しています。上の段は2つ間違っています。カードをドラッグして呼ぶ順番を入れ替えると、直るでしょうか。
