プログラミングと副作用と
はじめに
あまりしないことなんですが。
今回は少しだけ。
プログラミングの副作用について語っています。
ですが、私自身、この「プログラミングの副作用」について正確に把握できているのかどうかは心許ない。そのおつもりで読んで頂けると助かります。
もちろん、意見指摘反論など大歓迎です。
前書き ~Pythonのリスト内包表記と副作用と~
先日、Pythonのリスト内包表記について紹介しました。
リスト内包表記は次のようなものです。
a = [i for i in range(0, 4)]
次の表記と同じ意味です。
a = [0, 1, 2, 3]
C言語などですと、配列や構造体の初期化は後者のように数値を羅列するしかありません。
Pythonはそこに式を書けるようにしました。
このサンプルの場合は
a = [0, 1, 2, 3]
の方がわかりやすいので、リスト内包表記の有り難みを感じ難いかもしれません。ですが、式が書けるということはもっと高度な書き方もできるということです。例えば、このリストの要素の数が4つにとどまらす、100個、1000個となると、まさか1000の数値を列挙するわけにもいきません。その場合はリスト内包表記が威力を発揮します。
リスト内包表記のメリットはなんでしょうか。
よく言われることには、
(1)コード量が少ない
(2)処理速度が速い
というものがあります。
確かにそれがメリットであることも間違いではないのですが、リスト内包表記の一番のメリットは
(3)副作用がない
ということかと思います。
「副作用がない」とはどういうことか。
プログラミングにおける副作用については私もあまり詳しくない、ということを前提にしつつも、私なりの理解を綴ってみようかと思います。
副作用とはなにか
副作用が何かということそのものが難しい。
『主たる作用以外の作用を副作用という』
副作用という表現には良くないものというイメージがありますが、必ずしもそうではありません。『主たる作用以外の作用』という表現の中に、善悪を意味するものはありません。ですから「副作用=良くないもの」というのは少し早計です。
と言っておきながらすぐに否定するようで申し訳ないのですが、プログラミングにおいて副作用というのは好まれません。「副作用」というだけで否定されると言っても過言ではありません。何か関数を呼び出して、その呼び出した目的以外に別の効果があったとき、その「別の効果」が結果的に見て悪くないことであったとしても、良しとはされないのです。さらにその「別の効果」が「本来の目的」から離れているほど否定されることになります。それは、余計なお世話でしかなくなるわけです。プログラミングにおいて「副作用」は招かれざる客でしかありません。
プログラミングにおいて嫌われる副作用の代表格はなんでしょうか。
外部変数です。
常々、外部変数は嫌いでした。
「外部変数なんて、大っ嫌い!」
というくらいの感覚でコーディングしてもらって構いません。
「なんとか外部変数を使わずにきたけど、もう、ここは使わないとどうしようもない」
そういう時だけ外部変数を使えばいいでしょう。
プログラムをわかりにくくするような外部変数には、何度も泣かされてきました。
過去に私は外部変数を使わずにプログラミングしたことがあります。C言語の場合の「extern」を使わずにプログラミングをすることは可能です。
思わず、外部変数に熱くなってしまいました。
閑話休題。
それくらいに嫌いな外部変数でしたが、何故嫌いなのか、最近少しわかったような気がします。
そう。
副作用を招きやすいからです。
まずは、副作用のない関数から
例によって足し算プログラムを考えてみます。
Pythonで書きます。
簡単なコードなので、Python未経験の方でも容易に理解できると思います。
すぐに思い浮かぶのはこんなコードでしょうか、
def add(a, b):
return a + b
これは、副作用のないコードです。
同じ引数を使えば、いつも同じ結果を得られます。
そして関数を修正しなければならなくなった
ところがある日・・・
2値でなく、3値を足し算しなればならなくなったとします。でも、この関数「add」は既に100個も使われています。3値に変更しなければならないのは、そのうちたったの4個です。
こんな風に変更したら?
def add(a, b, c):
return (a + b + c)
まずい!
100個を全部修正しなきゃならなくなる!
変更したいのは、4個だけなのに!
Pythonなら、こんなこともできます。
def add(a, b, c=0):
return (a + b + c)
3番目の引数「c」は省略可能で、省略したら「0」となります。
これなら、引数2つだけでも
「add(1, 2, 0)」
となるので、影響はなしですね。
次のどちらの書き方もできます。
result = add(1, 2)
result = add(1, 2, 3)
デフォルト引数が使えるとは限らない
デフォルト引数が使えるPythonのような言語ならいいですが、C言語等ではデフォルト引数が書けません。
どうする!?
苦肉の策です。
c = 0
def add(a, b):
return (a + b + c)
これでどうでしょう。
2値でも。
result = add(1, 2)
3値でも。
c= 3
result = add(1, 2)
両方に対応できます。
・・・。
そう。
この「c」が副作用の元凶なのです。
こうして副作用を引き起こす外部変数が作られる
この関数仕様の場合、
#1
result = add(1, 2)
#2
c = 3
result = add(1, 2)
#3
result = add(1, 2)
このうち、#1 と #3 は同じように呼び出していますが、結果が違います。
#1 の結果は 3
#3 の結果は 6
です。
#2 のように関数を使ったことが、
#3 の結果に影響してしまったからです。
#2 は 1 と 2 と 3 を足し算したかっただけなのに、「c を 3 に変更する」という「副作用」を招いているのです。そして、c を 3 にしたことが、同じ関数を呼び出した別の処理に影響してしまいました。
c は必ず 0 に戻しておかなければなりません。
いや。
一緒にリンクして、別のスレッドや別のタスクがこの関数を使っていたとしたら、 0 にもどすだけではすみません。排他制御までが必要です。
排他制御!
さらに長くなるのでそれはまた別に。
つまるところ副作用とは?
「変数に値を代入する」
まさにそれが副作用です。
プログラムはメモリの上で動いています。
土台となるメモリが変わるとプログラムの動きも変わります。「変数に値を代入する」ということは「メモリを変更する」ということであり、その「メモリ上で動作しているプログラム」が動作の変更を余儀なくされるということは必然です。「変数に値を代入する」ということは、「状態を変更している」ことに他なりません。そして、その状態の変更が副作用そのものなのです。
外部変数は影響範囲が広大です。
いつでもだれでも参照できて、いつでもだれでも変更できます。そのため副作用の影響範囲も広い。
ですが、副作用は外部変数に限ったことではありません。ほんのちょっとのメモリでも、一瞬のオート変数でも、副作用に変わりありません。
実は、C言語の for ループは副作用があると言われています。それについてはこちらで。
足し算関数の修正方法
ちなみに。
先ほどのような状況で「足し算を2値から3値に変更する」というような変更に迫られた場合、
c = 0
def add(a, b):
return (a + b + c)
というような変更にするのではなく。
def add(a, b):
return (a + b)
def add3(a, b, c):
return (a + b + c)
のように、別の関数を用意するべきでしょう。
「a + b」に共通の修正が必要となった場合には、「add」と「add3」の2つを修正しなければなりません。でも、それもやむを得ません。
外部変数を使うくらいならその方がマシです。
そんな関数設計をしてしまった自分を恨みつつ、似たような関数を追加しましょう。
後書き
Pythonのリスト内包表記と副作用の関係については・・・いずれまた・・・。
この記事が気に入ったらサポートをしてみませんか?