関数型界隈で、「状態がある=副作用」みたいな話を何度か聴いてちょっと違和感があった。 副作用とは、主たる目的の他に外部に悪影響を与えることだと思っていたのだが。 つまり「Hello, World」を表示することは、あまり副作用と呼びたくない。(主作用?)
そこで、そもそも副作用がどんなもので何が悪かったのかを思い出してみた。
例
以下、ひたすら例を挙げて感想を述べていくことにする。 JavaScriptで記述しているが、任意の言語に読み替えて問題ない。
データの更新1
function completeTodo(todo) { todo.done = true; }
引数を書き換えているが、そういう関数だと思って使えば特に問題にはならない。
データの更新2
function completeTodo(todo) { todo.done = true; return todo; }
似ているがこっちは紛らわしい。新しいのは戻り値だけだと錯覚してしまう。
データの更新3
function completeTodo(todo) { todo.done = true; updateDB(todo); }
与えられた変数でDBを更新するパターン。変数がDBと同期しているという意図なのか、単なるミスなのか。
データの更新中にエラーが発生した
function completeTodo(todo) { todo.done = true; sometimesThrowError(); }
「データの更新1」の途中で失敗してしまったパターン。巻き戻しが必要。
ついでに何かを収集する
function calculate(a, b, warnings) { if(a === 0) { warnings.push('a is zero'); } return a + b; }
あるあるパターン。これはあまり怒っても仕方ない気がする。
計算過程で引数が変化する
function lastTodo(todos) { todos.reverse(); return todos[0]; }
これは完全にアウト。
計算過程で引数が変化するので戻した
function lastTodo(todos) { todos.reverse(); var last = todos[0]; todos.reverse(); return last; }
…。
危ういreduce1
var newValue = array.reduce(function(memo, data) { memo[data.id] = data; return memo; }, {});
JavaScriptだとこうなりがち。この時点では無害。
危ういreduce2
var oldValue = { '_1': 'foo' }; var newValue = array.reduce(function(memo, data) { memo[data.id] = data; return memo; }, oldValue);
oldValueとnewValueは同一変数。どうしてこうなった。
遅延読込み
var cache = {}; function findById(id) { if(cache[id]) { return cache[id]; } cache[id] = getFromDB(id); return cache[id]; } var data = findById('_1');
変数に影響を及ぼしてはいるが、特に問題はならない。
IDの自動生成
var n = 0; function nextId() { return '_' + n++; } function createTodo(name) { return { id: nextId(), name: name }; }
副作用と言えなくもないが、意図したものだと思う。
コンソール表示
function printTodo(todo) { console.log(todo.name); }
これは副作用とは言わないのではないか。
分類
危険度による分類
- バグ
- 危うい(書いた本人の認識が薄い)
- 危うい(書いた本人は意図しているが認識の共有が必要)
- 無害(設計パターン)
- 無害(状態更新を目的とした関数や画面表示)
副作用を及ぼす対象による分類
- 引数
- 関数外
- 言語外
まとめ
ヤバい奴らはやっぱりヤバい。危険な状況は単純に書き換えるか否かというよりも、意図せず起こしがちなパターンに潜んでいる。 それから、mutableなデータ構造がしばしばそのような状況を生み出していることは紛れもない事実だと思う。