1. はじめに
関数型プログラミング(FP)は、基本的な構成要素がオブジェクトでもプロシージャ/メソッドでもなく、数学的な意味での関数であるようなプログラミングパラダイムです。FPでは「何を計算するか」を記述することに重点が置かれ、「どのように計算するか」は二次的です。
ラムダ式や LINQ を使ったことがあれば、FPのアイデアの一部にはすでに触れています。でも違いはどこにあるのか? 実務的には:OOPはオブジェクトとその相互作用を記述し、手続き型は一連のステップを示し、FPは関数の合成、振る舞いを値として渡すこと、不変性(immutability)の採用、そして副作用の排除に焦点を当てます。
なぜ新しいパラダイムが必要なのか?
- よりクリーンで予測可能、テストしやすいコード。
- マルチスレッドのサポートが簡単になる(「状態がなければ問題もない」)。
- 簡潔さと表現力(コードが少なければバグも減る)。
- 高レベルで再利用しやすい抽象化。
アナロジー
レストランに「オムレツを作って」と注文が来たと想像してください。命令型のシェフは手順書通りに動きます:卵を取る、割る、かき混ぜる、焼く。関数型のシェフはこう言います:res = omlet(yaica) — 関数を操作して、キッチンの内部状態から抽象化します(まあ、完全ではないにせよ)。
C#では両方のアプローチが使えます。これが言語を実用的かつ強力にしています — 特に現実的なプロジェクトで。
FPの主要概念
1. 高階関数(関数を引数や戻り値にできること)
関数はパラメータとして渡したり、別の関数から返したり、変数に格納できます。ラムダ式やデリゲートで既にやっていることです。FPでは「関数の関数」が基礎になります。
2. 純粋関数
結果がパラメータだけに依存し、外部を変更しない(副作用がない)関数を「純粋」と呼びます。同じ引数なら常に同じ結果を返します。
3. 不変性(Immutability)
データは「その場で」変更されず、新しい状態は新しいオブジェクトとして表現されます。これによりプログラムの推論が簡単になり、並行処理でも有利です。
4. 副作用の不在
関数がファイルに書き込んだり、グローバル変数を変えたり、画面に何か描画したりしない — 単に結果を返すだけ。実際には副作用は避けられませんが、それらはシステムの端(エッジ)に隔離するのが一般的です。
5. 関数の合成
関数は他の関数から作れます、ブロックを積み上げるように。例えば正の数をフィルタして、それらを二乗して合計する。各操作は別々の関数で、簡単に組み合わせられます(Where → Select → Sum)。
2. C#におけるFP:理論から実践へ
C#はマルチパラダイム言語です:OOP、手続き型、そしてラムダ、デリゲート、拡張メソッド、LINQ を使った強力な関数型スタイルをうまくサポートします。
学習用アプリの例で見てみる
数値や文字列のリストを扱うプログラムを開発していると想定します。これらのデータに対して、関数型スタイルでさまざまな操作を適用するのが目的です。
例 1: 高階関数の利用
// リストのすべての要素に対してアクションを適用する
public static void ForEach<T>(List<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
使い方:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // 関数をパラメータとして渡している
見えますか? 関数は変数に「格納」したり、通常の値として渡したりできます — キッチンでリンゴを渡すみたいなものです。
例 2: 純粋関数
プログラムの状態を変更せず、引数だけに依存する関数:
int MultiplyByTwo(int x)
{
return x * 2;
}
- 外部に依存しない。
- 外部を変更しない。
- x = 5 なら常に 10 を返す。
これと対比して、グローバル変数を使って状態を変える関数:
int total = 0;
int AddToTotal(int x)
{
total += x;
return total;
}
これはもう純粋関数ではありません — 結果が外部状態に依存し、外部を変更してしまいます。
例 3: データの不変性
入力データを変更する代わりに新しいものを作る:
List<int> AddOneToEach(List<int> numbers)
{
return numbers.Select(n => n + 1).ToList();
}
入力のリストは全く変更されません。並行処理のあるプログラムでは特に便利です:ロックやデータ競合が減ります。
例 4: 関数の合成
全ての偶数の二乗の合計を求める:
int SumOfEvenSquares(List<int> numbers)
{
return numbers
.Where(n => n % 2 == 0) // 偶数だけ残す
.Select(n => n * n) // 二乗する
.Sum(); // 合計する
}
読みやすく宣言的:各操作が個別の関数として役割を持っている。
3. 便利な注意点
FP、LINQ、そしてC#
LINQ はコレクションに対する「実践的なFP」に近いものです:高階関数(Where, Select など)を使い、元のシーケンスをミューテートせず新しいシーケンスを得ます。各変換は個別の式であり、結果は IEnumerable<T> として「何を得たいか」を記述します、つまり「どう繰り返すか」ではありません。
類推表
| 命令型(手続き型/OO) | 関数型(LINQ/FPスタイル) |
|---|---|
|
|
| コレクションを「ミュート(変更)する」 | 新しいコレクションを得る |
| 状態(total += x) | 純粋関数(xs.Sum()) |
| 「これをしろ」と記述する | 「何を得たいか」を記述する |
FP vs OOP:二つの世界が一つのC#に共存する
対立する陣営ではありません。実際のC#プロジェクトでは両方を組み合わせます:ドメインモデルはクラス(OOP)で表現するのが都合が良いことが多く、コレクションの処理、データの集約や変換は LINQ、ラムダ、拡張メソッドを使った関数型スタイルで行います。
デリゲートに関する知識は直接役に立ちます:Func<T, TResult>、Predicate<T>、Action<T> はFPスタイルの典型的な構成要素です。
汎用フィルタ関数の例:
List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
result.Add(item);
}
return result;
}
呼び出し例:
var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);
さまざまな条件ごとに似たようなメソッドをたくさん作る代わりに、一つの汎用関数で済ませられます。
なぜ雇用者や面接でFPが評価されるのか?
- FPはシステム全体を起動しなくても小さなコードブロックをテストしやすくする。
- ロジックの保守が楽:状態が少なければバグの原因も減る。
- 並列や非同期コードを書きやすい:グローバル状態がなければデータ競合が減る。
「熱狂しすぎない」ために
FPは強力ですが、C#は純粋関数型言語ではなく、すべてのケースで完全な純粋性が必要なわけではありません。局所的な変数や適切な箇所での合理的なミューテーションを恐れないでください。重要なのは可読性、予測可能性、テストしやすさです。FPの要素は道具であり宗教ではありません。
4. 初心者が犯しやすい典型的なミス
一見関数型に見えるコードでも、本質的には関数型でないことはよくあります。
例えば、関数が新しいコレクションを返しているように見えて、内部で元のリストを変更していると、不変性の原則が破られ呼び出し元の期待を壊します。
別の例:ラムダ式が外部の変数にアクセスしてそれを変更する場合。これは副作用と見なされ、コードの振る舞いを予測しにくくします。
C#のコンパイラはこれを止めてくれません:言語はどちらも許容します。だからFPの実践では、関数が「自分自身で生きる」ように、外部を変更せず引数以外を読み取らないことを意識する必要があります。
GO TO FULL VERSION