関数型言語でよく話に出てくる「副作用」。今回は、その副作用についてまとめてみる。
まず、副作用とは「別の世界に与える影響」または「別の世界から受ける影響」のことです。
int function(int a, int b);
例えば、C言語で上記のように定義された関数について、同じ値の引数なら戻り値も必ず同じになるとき、この関数には副作用がありません。おそらく、2つの引数からなんらかの演算によって戻り値を算出する内容のコードになっているのでしょう。
void action(int a, int b);
しかし、上記のように戻り値が無い関数の場合、引数として入力された値はどこへ行ってしまったのでしょうか。この関数は何もしない関数なのでしょうか。もし何か処理を行っているとすれば、その結果はどこに出力されたのでしょうか。
これが、副作用を持つ関数です。この関数の処理結果は「別の世界」へと送られたのです。
「別の世界」をもう少し具体的に言うと、「自分と平行して動作しているもの」となります。
コンピュータでは、CPU、GPU、USBコントローラ、IDEコントローラ、Audioデバイスなどが、全て同時平行的に動作しています。CPU動作中はGPUが停止してて画面が表示されない、などということはありません。
つまり、全てのI/Oデバイスへのアクセスは副作用なのです。
また、1台のコンピュータにCPUが2つあった場合、2つのCPUは平行して動作します。そして、OSのカーネルが提供するマルチプロセスやマルチスレッドなどは、仮想的なCPUを作り出すサービスです。
つまり、プロセス間通信やスレッド間通信も副作用です。
さらに、下記のようなコードを書いた場合、
int a = 3;
int b = 4;
a = a + b + 5;
変数aは、再代入によって内容を書き替えています。これは、メインメモリをデータストレージデバイスのように扱っていることになるため、これもI/Oデバイスへのアクセスと同様に副作用です。ちなみに、変数bは再代入を行っておらず、実質的に定数であると考えられるため、これは副作用にはなりません。
副作用の伴うコードは、可能な限り排除しなければなりません。なぜなら、副作用はバグの発生と大きな関係があるからです。
例えば、I/Oデバイスへの書き込みは失敗するかもしれません。また、I/Oデバイスからはどんな値が読み込まれるか完全に予測することはできません。プログラムを組む際は、これら予想外の事態に配慮しなければなりません。もし、予想外の事態に対処できなければ、これがバグという形になって表れるからです。
この予想外への対応は、副作用を含むコードと共に行われます。そして、「予想外」に対応するコードは、大抵は面倒で書くのに時間の掛かるコードです。副作用があまりにも多いコードだと、この予想外への対応が困難になります。面倒だからといってここでコードを省略すると、それがバグに繋がってしまいます。
関数型言語では副作用を特別扱いするものが多く、副作用への対処がやりやすくなっています。逆に、手続き型言語では、副作用の存在自体が見えにくく対処しづらくなっています。







