2014-01-17

副作用

同僚に「から副作用とは何か教えろという趣旨の要求があった。この文書はその同僚に向けて書く。

副作用がある、というのは「知ることができるような状態変化が発生する」ということだ。定義から入ろうとすると、状態とはみたいな話になってしまうので、例をあげていく。サンプルコードはすべて Python で書く。

副作用のない関数呼び出し

>>> def add(a, b):
...    return a + b
... 
>>> add(1, 2)
3

関数 add の呼び出しには、副作用はない。関数は呼び出され、計算され、結果が返されているが、システムの状態に変化はない。関数が値を返すことは、副作用ではない。

代入

>>> VAL = 0

この代入文は副作用がある。この代入の実行前には VAL が定義されていないか、他の値が入っている。実行後には VAL に 0 が入っている。VAL の値が変化する = 状態変化がある = 副作用がある。

この代入の前にも VAL の値は 0 の場合もあり、状態は変わっていない。それは結果的にたまたま変わっていないだけであって、この代入文には VAL の値を書き換えるという副作用がある。

副作用のある関数呼び出し

>>> VAL = 0
>>> def incr_by(x):
...     VAL += x
...
>>> incr_by(4)
>>> VAL
4

incr_by 関数の呼び出しには、副作用がある。呼び出す前と後で、VAL の値が変わる = 状態変化がある = 副作用がある。incr_by に 0 を渡した場合は(r

副作用のあるメソッド呼び出し

>>> d = {'foo': 1, 'bar': 2}
>>> d.update({'baz': 999})
辞書の update 関数は副作用がある。上の例で、呼び出す前には辞書 d はキー baz を持っていない。update 関数を呼び出した後に、 d はキー baz を持つ。あるいは、最初から d['baz'] が定義されていても、値が書き換えられる。最初から d['baz']==999のとき(r

ファイル書き込み

>>> fout = open('iwata.txt', 'w')
>>> fout.write('hello')

上の例では open 関数も write メソッドも副作用がある。Python では、書き込みモードでファイルをオープンすると、そのファイルが作られる。したがって、呼び出し前後で os.path.exists() の戻り値が変わる。これはシステムに対する副作用である。

write メソッドを呼び出すと、ファイルに文字列が書き出される。write メソッドの前後で、open('iwata.txt', 'r').read() の戻り値が変わる。これも副作用である。

ところがどっこい

ここで「代入は副作用ではないが、ファイルへの書き込みは副作用である」なる発言を考える。VAL の例で代入は副作用である、という上の例と矛盾する。視点/立場をどこに置くか、で副作用の範囲が異なる。このエントリで例示してきたプログラムの視点で見ると、代入は副作用である。けれど、ファイルシステムだけを観測した場合には、変化がないので、副作用ではない。

最初の add 関数を呼び出すとき、足し算結果が欲しいプログラムのレイヤで観測すると副作用はない。しかし、実際には呼び出し前後で、内部のメモリ状態には変化が起こり、inspect ライブラリを使えば変化を知ることもできる。その立場で観測すれば副作用がある、と言える。

というわけで

知覚できる状態を変えてしまうとき、その関数、メソッド、操作は副作用がある、という。ただし「誰/何にとって」なのかによって、解釈が異なる場合がある。

BDD 的なアプローチでテストを書くとき、通常 given の操作は、テスト対象を特定の状態に強制するため、副作用が起こりえる。when 操作に副作用がないときには、then で戻り値を確認すればよい(副作用がないことを確認したければ、他にもすることはある)。when 操作に副作用があるときには、戻り値の確認だけでは不十分で、副作用を検出(他のAPIを呼び出して変化していることを確認など)する必要がある。