プログラミングは科学であると同時に、芸術でもあります。 プログラミングのスタイルには好みが大きくかかわってくるため、仲間内で議論になることも珍しくありません。 かねてより議論されているテーマの 1 つに、プログラミング パラダイムとして関数型プログラミングオブジェクト指向プログラミングのどちらを選ぶべきかというものがあります。 どちらの方が優れていて、 どちらを選ぶべきなのでしょう?

関数型プログラミング派も、オブジェクト指向プログラミング派も、自分の手法にはほぼどんなときにも役立つ明確なメリットがあるとアピールしています。 しかし、適している手法は場合によって異なると考える人もいるでしょう。 この記事では、両方のプログラミング手法について考察し、事実とウソの線引きをします。 最後までご覧になれば、これら 2 つの手法に対する理解を深め、状況に応じて適切に使い分けられるようになることでしょう。

関数型プログラミングとは?

関数型プログラミング (FP) とは、たいへん歴史の長い、おそらくは最古のプログラミング手法であり、 関数を主軸とするソフトウェア開発プロセスを指します。 FP では、モジュール間で状態を共有したり、データが変更されること(可変性) を気にしたりしなくても済むように、関数を組み合わせることで新しい関数を作成し、アプリケーションを開発します。 このため、FP は命令的ではなく宣言的に使用するのが一般的です。

つまり、どういうことなのでしょう?

関数型プログラミングの主な概念

まず、関数は FP における “第一級オブジェクト” であると理解してください。関数は他のすべての値と同じように扱われるので、 引数として渡すことも、他の関数から戻り値として返すことも可能です。 関数を返す関数のことを(返す値の抽象度が高いことから)高階関数と呼びます。 高階関数を使用すると、引数から直接新しい関数を作成できます。

2 つ目に重要なのが不変性という概念です。 不変な値とは、プリミティブ、つまり変更できない値のことです。 たとえば、数値は不変と見なされるので、 42 を 14 に変更することはできません。 唯一行える操作は、14 という値を持つ新しい数を作成して、それまで使用していた変数にこの新しい数を代入することだけです。 ですが、このように 14 という新しい数を作成しても、それまで使用していた 42 という数には影響しません。

このような値の扱い方は、数のようなプリミティブ値では理にかなっていても、オブジェクトや配列などの合成された値については奇妙に感じられるかもしれません。 それでも、FP の原則ではすべての値を不変として扱います。 値を変更するには、作成済みの値を基本値やコピーとして使用するなどして、新しい値を作成するしかありません。

FP では、不変データ型を導入することで、純粋関数を実現できます。 純粋関数とは、引数によってのみ定義される関数です。 引数は変更できないので、純粋関数は予想どおりに動作することが保証されます。 つまり、引数が同じであれば、返される結果も常に同じです。 他のプログラミング手法では、このような挙動の予測可能性は保証されません。

関数型プログラミングが向いているユース ケース

FP は元々科学分野を中心として利用されていましたが、今ではさまざまな分野に広がっています。 たとえば Web 開発では、React というユーザー インターフェイス (UI) ライブラリにおいて、宣言的で扱いやすくすることを目的として FP の原則が採用されています。 このライブラリでは、アプリケーションの現在の状態を変更することなく反映するため、不変の状態オブジェクト (値) を使用することが提唱されています。 つまり、新しい状態が必要なら、新しいオブジェクトを作成しなければなりません。 このアプローチの利点は、UI の変更を 1 つ 1 つさかのぼれることです。 それまでの状態はすべて、手付かずのまま使用可能になっているからです。

FP のルーツがコンピューター科学の学術的な面にあることは確かですが、実用面での影響は Lisp や Scheme などの言語までさかのぼります。 上述のような特性は JavaScript 言語にもいくつか組み込まれており、FP が古くから使用され続けている大きな要因の 1 つとなっています。 一方、FP の原則を基盤として構築されている言語もあります。その筆頭は、F#、Clojure、Elixir などです。 学界では、Haskell が数十年にわたって定番の言語とみなされてきました。

オブジェクト指向プログラミング(OOP)とは?

関数型プログラミング(OOP)とは古くから存在しているものの、オブジェクト指向プログラミング (OOP) の方が伝統があると考える開発者もいます。 OOP の発明は 1970 年代後半。そして、OOP を世に広めたのが、Smalltalk や Objective-C などのプログラミング言語 です。 それから C++、Java、C# を通じて、このプログラミング スタイルは多くの開発者の心に浸透し続けてきました。

以下に、OOP の重要な原則と最も一般的なユース ケースを紹介します。

オブジェクト指向プログラミングの主な概念

OOP では、相互に通信可能なオブジェクトのコレクションとして、ソフトウェア アプリケーションをモデル化します。 各オブジェクトのインターフェイスはクラスになります。これは、関数と値がどのインスタンスからでもアクセス可能であることを示すテンプレートです。 当初、この通信機能の構想は、大まかに言えばネットワーク通信や非同期 IO を可能にするメッセージ パッシングでした。しかし、しばらくしてから、対応するオブジェクト上で呼び出されるシンプルな関数として普及しました。 このような関数をメソッドといいます。

FP ではオブジェクトが不変でしたが、OOP の場合、オブジェクトの可変性が性質の 1 つです。 このため、メソッドを呼び出すと、オブジェクトの値も変わるケースがほとんどです。

特にプログラミングを教える場合に、OOP が多くの開発者に使用されている理由の 1 つは、命令的に記述されているためです。 つまり、どこで何が起きているのかを明確に示せるというわけです。 OOP への反対意見として、命令的なスタイルを使用したとしても、オブジェクトそれぞれについて現在の状態は判断しにくいことがある、という指摘があります。 ここにオブジェクトの変化する性質が合わさると、たちまち結果が予測できないものになりかねません。

オブジェクト指向プログラミングが向いているユース ケース

開発現場では従来、ほぼすべての UI が OOP のアプローチで作成されてきました。 クラスベースのコンポーネントは、他の類似コンポーネントから基本構造 (フィールドやメソッド) を継承できるので、この手法は比較的簡単でもあります。 たとえば、日付入力フィールドはテキスト入力フィールドから継承させ、テキスト入力フィールドは入力ボックスから継承させ、入力ボックスはコントロールから継承させる、という具合です。 この継承ベースのアプローチであれば、後は追加のメソッドを指定して、いくつかの既存のメソッド上で挙動を再実装するだけです。 キーボードやマウスなどの処理のロジックを再び記述する必要はありません。

今や、OOP はあらゆる汎用プログラミング言語において必須の機能となっています。 F# といった FP ベースの言語ですらも、クラスや継承などの OOP の機能が直接サポートされています。 その好例が JavaScript で、クラスや継承などの標準機能は即座に組み込まれはしなかったものの、最近のバージョンで追加されました。

このように、2 つのプログラミング アプローチには、必要に応じた使い分けがうなずけるほど十分な違いがあります。 では、どちらを、どのようなときに使えばよいのでしょうか? それぞれの派閥の意見を見てみましょう。

関数型プログラミング vs オブジェクト指向プログラミング論争

ここまで、どちらの手法にもそれぞれのメリットとデメリットがあることを説明してきました。 コンピューター科学の教授 Norman Ramsey 氏は、有名な Stack Overflow での回答で耳寄りな考え方を教えてくれています。 いわく、FP はすべてのオブジェクトが既知であるが、挙動の変わる可能性がある場合に優れている。 反対に、OOP は挙動が既知であるが、実際のデータ型の変わる可能性がある場合に優れている、ということです。

FP 派と OOP 派のどちらの意見も、Ramsey 氏の考えにはとどまりません。 たとえば、FP 派は、「ソフトウェアを純粋な FP スタイルで記述してクリーンに簡潔に設計すれば、デバッグしやすくなり決してクラッシュしない」、 「FP なら、テスト駆動開発のメリットのいくつかが自動的に得られる」、「SOLID 原則のすべてが厳密に適用された OOP は実質 FP」と主張しています。 SOLID 原則に従うと、FP とほとんど同じような関数を使用することになるのは事実ですが、FP でなければならない理由はありません。 たとえば、どの SOLID 原則も、データの可変性は禁止していません。

OOP 派の中には、パフォーマンスや簡潔さが損なわれるからという理由で、FP のいくつかの利点を軽視する意見もあります。 「オブジェクトのフィールドを 1 つ変更したいだけなのに、なぜすべてのフィールドを新しいオブジェクトにコピーしなければならないのか」、 「要素を 1 つ設定したいだけなのに、100 万個の要素を持つ配列をコピーしなければならないなんて」というものです。 もちろん、コピーを作成しなくてよいパターンもありますが、その場合でも再び特別なデータ構造が必要になります。 このレベルの間接参照は、OOP とは関係なく、長年使用してきた直接的なアプローチと比べて不可解に感じられるかもしれません。

これら 2 つのアプローチには非常に対照的な面があるものの、お互いを補い合うことも可能です。 単一のソフトウェア アプリケーションで複数のクラスを使用することを禁止するルールはありません。 また、アプリケーションの全般的な状態を変更可能にしなければならないというルールもありません。

将来を見据えて

人気のあるプログラミング言語は、ほぼすべてがマルチパラダイムです。 どれもが、関数を渡せるようにしたり、データ オブジェクトの不変性に対処するヘルパーを用意したりすることで、FP をサポートしています。 同時に、クラスや継承といった OOP の機能も備えています。 いずれにせよ、このマルチパラダイムのアプローチはこれからも続くことでしょう。 ユーザーからすれば、基本的に選択肢は多ければ多いほど便利です。

一般的には、OOP よりも FP 向けの機能が追加される傾向があるようです。 データ オブジェクトの複製、関数の参照、関数の合成を追加でサポートした言語なら、FP をも超えた頼れる存在になります。 C# などの言語では、FP 向けの機能があれば OOP の機能が強化されます。 この組み合わせは、たとえば特定のヘルパー関数を使用しているときなど、OOP で開発されたアプリケーションでも役立ちます。

ただし、FP と OOP のアプローチを組み合わせられるようになるまでには時間がかかり、 これが理由で C# を避ける開発者もいます。 初めは設計がまっさらかつ上品であり、Java の代替であったはずの言語が、C++ に似た複雑怪奇なものになることもあるかもしれません。

OOP は永遠に不滅です。とは言え、FP と共存する未来が訪れることはまず間違いありません。 開発から副作用をなくすという理想は、現実に実現するのは明らかにほぼ不可能です。 副作用は、ログ メッセージをコンソールに出力した時点で既に出現しているものだからです。 かつて、FP の実用性は開発者が見つけて引き出さなければならないものでした。 しかし、FP ならではの実用性が認識され始めた現在では、FP の機能を再び排除する理由はほとんどありません。

オブジェクト指向プログラミングの継続的なサポート

このような状況においても、OOP が現実的な選択肢であり続けているのはなぜでしょう? その答えは、OOP が本質的に理想のツールであるからです。 OOP の手法なら、モデル化できないものはほとんどありません。 OOP で FP の機能のほとんどをモデル化することさえ可能です。

たとえば、関数を渡す、結合するというような基本動作を考えてみてください。 こうした動作は、簡単に言えば、関手 (ファンクター)委譲 (デリゲート)で実現できます。 つまり、単一のメソッドを持つクラスに対応したオブジェクトに他なりません。

共通点を探そう

アプローチを切り替えるためだけに、アプリケーションの記述方法やソフトウェアを変える開発チームはほとんどないでしょう。 それよりも、プログラミング言語の最新の機能を使用するために、最も重要なアプリケーションのいくつかを積極的にリファクタリングするケースが一般的です。

どちらにしても、将来的にはさらなるハイブリッド化が進んでいくことでしょう。 未来のプロジェクトでは、FP 発のスタイルを OOP アプリケーションで使用したり、OOP の機能を FP 駆動型のアプリケーションで使用したりするようになっているかもしれません。

おわりに

アプリケーションには、FP が適しているものもあれば (例: コンパイラ)、OOP の原則が適しているものもあります (例: 従来型のデスクトップ アプリケーション)。 最適なプログラミング アプローチを決めるには、対象のプログラミング言語、フレームワーク、開発中のアプリケーションの種類など、複数の要因を考慮しなくてはなりません。 最も使いやすいものを選んで構いませんが、選択肢を増やすために知識をアップデートし続けることを忘れないでください。 どのような問題に挑む場合でも、解決方法が多いに越したことはないのですから。