プログラミング関係の資料を読んでいると、「副作用」という言葉を見かけるけど、「言葉の意味」と「なぜ話題になるのか」がわかりませんでした。
なので「副作用の定義」と「副作用が話題になる理由=気を付けるべきこと」について調べたことを簡単に書いておきます。
厳密さや詳しさよりも簡単にイメージが掴めればいいなという方針で書いてます。
より詳細に知りたい人は、参照記事などを読んでください。
目次
プログラミングにおける副作用の定義
一般的な会話における副作用の定義
一般的な会話において、副作用の定義というと、
1 薬物の、病気を治す作用とは別の、望んでいない作用。有害なことが多い。「副作用を伴う特効薬」
2 転じて、問題解決のためにとった手段によって起こる損害。「金融政策の副作用」
goo 辞書 副作用
と書かれいています。
簡単に言うと、副作用とは「本来想定している作用」とは別に発生する作用のこと
上記の意味で通じるかと思います。
プログラミングにおける副作用の定義
では、プログラミングにおける副作用の定義はというと、
ある機能がコンピュータの(論理的な)状態を変化させ、それ以降で得られる結果に影響を与えることをいう。代表的な例は変数への値の代入である。
Wikipedia 副作用 (プログラム)
と書かれています。
プログラム関係の書籍を読んでも、「ある処理において、状態を参照あるいは操作することで、次回以降の結果にまで影響を与える効果のことを副作用と呼ぶ」
と、似たようなことが書かれています。
これだけだと意味がわからないので、一般的な副作用の定義と、プログラムの実例を交えて説明していきます。
プログラミングにおける副作用を理解するポイント
上記の「副作用の定義」をプログラミングにおける副作用を理解する上で、ポイントとなるのは以下の3点でしょう。
- 「本来の作用」とは何を指しているのか
- 「想定外の作用」とは何を指しているのか
- 「状態」とは何を指しているのか
一次関数の例を用いての副作用のポイント説明
まず、わかりやすい例として、関数から上記3つを説明してます。
(純粋)関数における、「本来の想定作用」とは、「入力値(引数)」をもらって、「出力値(返り値)」を出す処理のことです。(純粋関数とは、副作用がない関数のことです)
f(x) = 2x という関数なら、「入力値をx=2」とすれば「出力値は4」という処理が「本来の想定作用」になります。
中学校で習う関数で、 f(x) = ax + b を考えてみます。
この関数は、正確に純粋関数として書くなら f(x,a,b) = ax + b
とするのが、本来は正しいです。 参照:プログラミングで言う「副作用」とは何ですか?
xだけでなく、実際はa,bも自由に値を決めれられる変数だからです。
不純な関数である f(x) = ax + b をプログラミングで書くと、下記のようになります。
var a = 1
var b = 1
const ichiji_kansuu = x => a*x+b;
ichiji_kansuu(1) // 2
a = 2
b = 2
ichiji_kansuu(1) // 4
上記のichiji_kansuuは、xという渡された「引数(入力値)」以外のa,bという「関数の外部の変数(状態)の影響」を受けています。
関数において、 「想定外の作用」とは、 関数の外部の変数の影響のことになります。
また、「状態」というのは、(現実世界における)ある時間Aとある時間Bで、「変わる可能性がある値 = 変数」のことです。
上記の ax + b という関数においては、a,bが「状態」であり、 「想定外の作用」をしているということになります。
副作用のわかりづらい部分の説明用サンプルプログラム
自分の場合、上記の説明だけだと、疑問が残りましたので、自分が理解に苦しんだ部分について、もう少し説明します。
副作用の説明用に、下記のプログラムを想定します。
var sample = {
x: 1, // 変数への代入(初期化)
plus_1(param) {
return 1 + param;
},
plus_1_with_side_effects(param) {
this.x = this.x + 1; // メソッド外の変数(状態)への代入(操作)=外部への影響=副作用
return this.x + param; // メソッド外の変数(状態)を参照=外部からの影響=副作用(副原因)
},
plus_1_with_no_side_effects(param) {
let y = 1; // メソッド内の変数への代入(初期化)
y = y + 1; // メソッド内の変数への参照・代入
return 1 + param;
},
print_currentSeconds_with_side_effects() {
console.log(new Date().getSeconds()) // メソッド外のオブジェクト、変数(状態)を参照=外部からの影響=副作用(副原因)
}
}
sample.plus_1(1); // 2
sample.plus_1_with_side_effects(1) // 2
sample.plus_1_with_no_side_effects(1) // 2
もう一回実行する。
sample.plus_1(1); // 2
sample.plus_1_with_side_effects(1) // 3 ← 前回から結果が変わっている=前回の処理の影響を受けている=副作用あり
sample.plus_1_with_no_side_effects(1) // 2
代入文から考える、副作用の範囲(視点)と副作用という言葉のわかりづらさ
代入文における、本来の想定作用とは、
var x =1;
だったら、xに1という値を代入することです。
で、副作用としては、xに1という値が入ったまま、ということです。
x=1という状態を作り出しています。
後続処理で参照や操作をされる可能性が生まれます。
上記のサンプルプログラムの例だと、sampleオブジェクト内で xという変数(状態)の影響を受けるということです。
{ var x =1; }
のようにすぐにブロックが閉じるのだったら、xという値は代入はされますが、すぐに消滅しますので、xの変数はどこからも参照、操作される可能性はありません。
つまり、変数xの状態は生まれませんので、(プログラミング上)副作用はないと言えます。
副作用の範囲(視点)
おわかりかと思うのですが、上記のサンプルプログラム例で言うと、xの副作用の範囲はsampleオブジェクト内に限られるということです。
どの範囲(視点)で見るかによって、副作用なのかどうかは変わってくるということです。
例えば、サンプルプログラム例の let y = 1; の部分は、
plus_1_with_no_side_effects 関数の範囲においては副作用と言えますが、
sampleオブジェクトにとっては、副作用ではありません。
副作用という言葉のわかりづらさの原因=副作用そのものが目的?
「手続き型(命令型)プログラミングは、副作用によってプログラムを組むプログラミングパラダイムである」 みたいな文章を初めて読んだときに、自分はすごく混乱しました。
手続き型言語の単位は「文」
参照: 「式と文、評価と実行、そして副作用」の記事の続き
文は一文一文ごとに「実行」される。実行の結果は「副作用」によってのみ表現できる
副作用とは「世界を汚すこと」。手続き型言語は副作用の連続でプログラムが進行していく
汚せる範囲を「スコープ」と呼ぶ
「想定外の作用」のはずなのに、それが「目的」ってどういうことなんだろう?
手続き(プログラムの実行単位)が「副作用」そのものを目的としている ってどういうことなんだろう? って。
わかりづらいなと思いました。
わかりずらさの原因は「プログラミングにおける作用」と「プログラミングにおける目的」が別物だという認識があいまいだったからです。
なぜか代入文における「本来の想定作用」とかを説明してくれている資料に出会えなかったので悩みました……。
{ var x =1; } の説明を読んだくれた人なら、どういうことか分かってくれると思います。
変数に代入だけして、すぐにその変数を捨ててしまったら、何も意味がないということですよね。
上記のサンプルプログラム例の x=1 は
plus_1_with_side_effects関数で(副作用の説明のために)使用されることを「目的」としています。
後続処理で、「状態=値を代入された変数」を参照・操作するために、代入文などの文は使用されるはずだということです。
なので、「手続き型(命令型)プログラミングは、副作用によってプログラムを組むプログラミングパラダイムである」みたいな文章は別におかしくないわけです。
副作用という言葉のわかりづらさの原因=2種類の副作用がある?
自分だけかもしれないのですが、「想定外の作用」という言葉を聞くと、「想定外の影響を与える」ということだと、思ってしまっていました。
plus_1_with_side_effects(param) {
this.x = this.x + 1; // メソッド外の変数(状態)への代入(操作)=外部への影響=副作用
return this.x + param; // メソッド外の変数(状態)を参照=外部からの影響=副作用(副原因)
}
サンプルプログラムのこの部分のとおり、「副作用」には、「状態への操作=外部への影響」と「状態の参照=外部からの影響」の2種類あるんだということです。
これを考えられなかったので、ドキュメントを読んでいて「副作用」という言葉の使われ方に混乱したことがありました。
つまるところ、純粋じゃない関数(メソッド)には、2組の入出力があるということになります。たぶん。 参照:「関数型プログラミングって何?」日本語訳
1つは、引数と返り値。(明示的)
もう1つは、副作用。(暗黙的)
プログラミングにおいて、副作用が話題になる理由【扱いには要注意】
プログラミングにおいて、副作用が話題になる理由について、簡単に書いておきます。
副作用は読解・改修を大変にし、バグになりやすい
副作用とは、「状態=値が変わりうる変数」を暗黙的に参照・操作することと同義です。
これは、長いプログラムの場合、
処理の読解や改修をしようとすれば、
その変数の値がどこで書き変わっているのか、どこで読みだされているのか、
影響範囲を全て確認する必要が生まれます。
手間です。影響範囲の見積りをミスしてバグの原因にもなりやすいです。
だから、「変数のスコープはできるだけ短くしよう」となります。
副作用は処理の独立を阻害するので、改修もテストも並列処理も大変になる
副作用とは、関数などの着目処理の、外部の(プログラムの)状態に依存していることと同義です。
つまり、関数を修正しようとしたら、外部のどこと暗黙につながっているのかを調査をする必要があります。 純粋関数ならば 調査は不要です。
また、純粋関数ならば、引数さえわかっていればいいので、部品として簡単に再利用することもできます。
テストのときも、外部の状態に依存していると、テストデータを用意するのが大変になります。
そして、並列処理にしたいときも、外部の変数に依存していると、その外部の変数が他の処理によって書き換えられないかどうかなどを気にする必要が生まれてきます。
副作用の扱いには注意しよう
ということで、副作用は便利な存在でもあるのですが、危険な存在でもあります。
便利だからといって乱用すると、後で苦しむことになります。
副作用のことを気にしていなかった人は、副作用の及ぼす影響について意識しながらプログラミングすると、読解・改修のときにラクできるかなと思います。