はじめに
関数を単に実装することは誰でもできることですが、関数を高品質に設計するにはそれなりのスキルが必要です。関数の品質が悪いと、それを利用する側のコード品質も低下し、結果的にコード全体の品質が悪くなります。したがって、たかが関数と思われがちですが、実際には関数が非常に重要な役割を果たしていることを理解し、適切なスキルを身につけることが重要です。
分けるは分かる
プログラムのコードに限らず、あらゆるものにおいて「分けることは理解することにつながる」といえます。そこで、プログラムの関数を作成またはリファクタリングする際に効果的な 4 つの「分ける」を紹介します。
フェーズを分ける
典型的な例として、バックエンドが提供する API として、クライアントサイドでフォーム画面に入力された情報を HTTP リクエストで受け取り、それをデータベースに登録するという処理を想定します。この種の処理では、一般的なパターンが決まっており、認可処理、フォームのバリデーションチェック、ビジネスロジックチェック、入力フォームの内容を内部モデルに変換すること、そしてデータベースへの登録が含まれます。
しかし、データベースへの登録直前に認可処理を行ったり、フォームのバリデーションチェックが完了していないのにビジネスロジックチェックを行ったりすると、混乱が生じることがあります。フェーズ分けがされていない場合、開発者は一連の処理全体を同時に考慮し続ける必要があります。これによって、思考の負担が大きくなることがあります。その結果、バグを作り込む根本原因ともなることがあるのです。
そのため、まずはフェーズを分け、各フェーズで関心事に集中できるようにすることが重要です。これにより、処理の流れが明確になり、効率的な開発やメンテナンスが実現できます。
副作用(エフェクト)を分ける
まず、誤解がないように申し上げますが、一連の処理から副作用を完全に無くすことは不可能です。あくまで分離が目的です。副作用を分離する目的は、その関数をエフェクトレスな関数にするためです。エフェクトレスな関数であることは純粋関数であることの必要条件です。ここで分離すべき副作用の例としては、データベースへの登録処理やメール送信処理などが挙げられます。
関数シグネチャの設計におけるポイントは、関数が何らかの値を返す場合、すなわち主作用がある場合は副作用を持たせないことが望ましいです。逆に、副作用を実行することが目的の関数では、主作用が存在しないこと、すなわち関数の戻り値がないことを関数シグネチャを通じて示唆することが重要です。
状態依存(ステート)を分ける
関数から状態を分離する目的は、その関数をステートレスな関数にするためです。ステートレスな関数であることは、純粋関数であることの必要条件です。ステートレスな関数とは、出力が入力にのみ依存するもので、同じ入力パラメータを渡すと毎回同じ出力が返されるものです。
例えば、関数内でシステム日時という OS の状態を参照し、それを基に出力が決定される場合、それはステートレスな関数とは言えません。このような場合は、システム日時を入力パラメータとして受け取ることで、状態を分離することができます。
インターフェースと実装を分ける
同じ処理を繰り返し行う場合や、複数箇所で同じ処理を実行する際、関数を作成することがよく行われます。ただし、このような状況では、実装に重点を置いたボトムアップな関数が作成されがちです。結果として、関数利用者が実装を調べなければ使えない関数が作られることがあります。
良い関数設計では、関数シグネチャからその目的を推測できることが大切です。利用者は「どのように実行されるか」よりも「何をするか」に関心があります。これがカプセル化の重要性です。
「何をするか」はインターフェースが担当し、「どのように実行するか」は実装が担当します。そのため、まずインターフェースを設計し、トップダウンな考え方で関数設計を行うことで、品質を向上させることができます。実装が隠蔽され、利用者が確認できない場合、関数の目的を判断する方法は関数シグネチャとコメントだけです。だからこそ、関数名が重要です。適切な関数名が付けられない場合、前述の 3 つの要素がうまく分離されていない可能性があります。
純粋関数への昇華
純粋関数を説明する際に、「参照透過性」と「副作用」という用語が登場し、説明が難解になると思われるため、ここでわかりやすく説明します。
まず鍵となる用語の定義ですが、
- 「作用」とは、関数が外部に与えるあらゆる影響を指します。
- 「主作用」とは、関数が戻り値を返すことを指します。
- 「副作用」とは、主作用以外の作用を指します。
- 「参照透過性」とは、式の作用を維持して、式と式の評価値を置換できる性質です。
- 「純粋関数」とは、参照透過性を持つ関数です。
参照透過性とは、「式の作用を維持して、式と式の評価値を置換できる」という性質のことです。例として、単純な関数 f(a, b) = a + b があるとき、2 と 3 の和を求めるには x = f(2, 3) とします。このとき、x = 5 と置き換えてもプログラム全体に影響が出ないとすれば、その関数は参照透過性があるということになります。前述の例においては置換しても式の作用はそのままであるため、関数 f は参照透過であると言えます。
次に、副作用を持つ関数 g(a, b)について考えます。これは関数 f と同様に和を算出しつつ、標準出力を実行するものとします。このとき x = g(2, 3) を x = 5 に置換した場合、副作用である標準出力が実行されなくなります。つまり、式の副作用が維持できていないため、関数 g は参照透過ではありません。
さらに、状態変数に依存する関数 h(a, b) = a + b + c について考えます。関数 f と同様に和を算出しつつ、何らかの状態変数 c の値をさらに加算するものです。わかりやすいように、c の値は午前なら 0、午後なら 1 を返すとしましょう。このとき x = h(2, 3) を x = 5 に置換した場合、午前においては問題ないものの、午後になったら関数の主作用が異なってしまいます。つまり、式の主作用が維持できなくなってしまうため、関数 h は参照透過ではありません。
結論として、参照透過であるためには、ステートレスかつエフェクトレスであることが必要十分条件です。つまり、関数から状態依存と副作用の両方を分離することで、それは純粋関数に昇華されるということです。
よく「同じ入力に対して同じ出力を返すなら参照透過である」という文言を見かけますが、これは正確ではありません。これは参照透過であることではなく、ステートレスであることの性質だからです。ただし、「参照透過であるならば同じ入力に対して同じ出力を返す」というのは正しいです。なぜなら、参照透過であるならばステートレスであるためです。
おわりに
関数設計の品質向上は、プログラム全体の品質に大きく影響します。直結します。本記事で紹介した 4 つのポイントを意識して関数設計を行うことで、効率的な開発やメンテナンスが可能になります。また、純粋関数について理解し、その特性を活かすことで、さらなる品質向上が期待できます。関数設計に関するスキルを磨き、より高品質なコードを書くことを目指しましょう。