プログラミングにおいて、式の評価は単に値を計算するだけでなく、プログラムの状態に影響を与えることがあります。この影響は、
主作用と
副作用の二つに分類されます。
主作用とは、式が評価される主な目的、つまり値を計算し返すことです。関数であれば、引数を受け取り、その結果を返すという行為が主作用にあたります。一方、
副作用とは、式の評価によって、プログラムの論理的な状態(グローバル変数、静的ローカル変数の値など、ローカル環境以外の状態)が変化することです。具体的な例としては、グローバル変数や静的ローカル変数の値を変更する、ファイルへの読み書きを行う(I/O処理)、などが挙げられます。
例えば、数学関数である正弦(sin)、余弦(cos)、平方根(sqrt)などは、通常、関数内部でローカル変数の変更程度しか行われず、プログラムの状態を変化させません。したがって、これらの関数には副作用がないとみなされます。
副作用を持つ機能は、その後のプログラムの動作に影響を与えます。なぜなら、プログラムの状態が変化することによって、同じ入力に対して異なる結果が生じる可能性があるからです。
副作用の有無を理解するために、簡単な例を考えてみましょう。
副作用のない機能の例:
与えられた数を2倍にする関数`double`を考えてみます。
double: x -> 2 x
例:
4 <- double: 2
この関数は、入力された値を2倍にして返すだけで、プログラムの状態を変更しません。この関数は、同じ入力を与えれば常に同じ結果を返し、他の機能の動作に影響を与えることもありません。このような性質を
参照透過性といい、副作用がない機能の特徴です。
副作用のある機能の例:
次に、グローバル変数`e`の値を参照し、それを変更する関数`add`を考えてみましょう。
add: x -> e: e + x
例:
e: 1
2 <- add: 1
2 <- e
この関数`add`は、引数`x`とグローバル変数`e`の値を足したものを返すと同時に、グローバル変数`e`の値を変化させます。そのため、この関数を実行するたびに、同じ引数を渡しても、返り値が異なったり、グローバル変数eの値が変わるので、他の処理の結果に影響を与えてしまいます。したがって、`add`関数は副作用を持つと言えます。
副作用とプログラミングパラダイム:
副作用は、
ノイマン型コンピュータアーキテクチャの基本概念である「状態」に深く関連しています。ほとんどの
プログラミング言語では、グローバル変数への代入、参照渡しされた引数の変更、インスタンスのメソッドなど、副作用を伴う操作が可能です。一方で、関数型
プログラミング言語では、原則として副作用を排除し、状態の変更を抽象化するためにモナドなどの手法が用いられます。
副作用を持たないことのメリット:
機能が副作用を持たないことは、以下の点で大きな利点があります。
形式的な検証可能性: 同じ入力に対して常に同じ結果が得られるため、
数理論理学に基づいた形式的な検証が可能になります。
バグの抑制: プログラムの状態に依存しないため、状況に左右される
バグの発生を抑えることができます。
宣言型プログラミング: プログラムがどのように動作するかではなく、何を達成したいのかを記述する宣言型プログラミングが容易になります。
副作用を持たないことのデメリット:
一方で、副作用を持たない言語設計は、
ノイマン型アーキテクチャとは相性が悪く、効率の面で不利になることがあります。また、単純な逐次処理を行う場合は、状態を中心に命令的な思考をする方が扱いやすい場合もあります。
まとめ:
副作用は、プログラムの状態を変化させる重要な概念であり、プログラムの動作に大きな影響を与えます。副作用を理解し、適切に管理することは、堅牢で保守性の高いプログラムを作成するために不可欠です。副作用を持たない機能(純粋関数)は、プログラムの理解やテストを容易にする一方で、効率性の観点から課題があることもあります。そのため、
プログラミング言語によっては副作用を許容する設計になっていることもあり、それらを適切に使い分けることが重要です。
関連事項
*
イミュータブル: オブジェクトが生成された後にその状態を変更できない性質のこと。副作用を避けるための重要な概念です。