kakudooo docs

プログラミングにおける副作用

In computer science, an operation or expression is said to have a side effect if it has any observable effect other than its primary effect of reading the value of its arguments and returning a value to the invoker of the operation.

参照: Side effect (computer science)

ある操作や式が「引数の値を読み取り、呼び出し元に値を返す」という主要な効果以外に観測可能な効果を持つ場合、その操作や式は副作用を持つと言われる。

コンピューターサイエンスの文脈だと上記のように定義される。

関数における副作用

コンピューターサイエンスの文脈では、特にプログラミング言語やそれらを使用した設計/実装の文脈で、副作用について言及されることが多い。

Nowadays, we define a side effect to be any state change that outlives the function, even if that state change is the primary intent of the function. In other words, a function with a side effect is impure.

Clean Code: A Handbook of Agile Software Craftsmanship, 2nd Edition 8章 では

と定義されている。

Clean Codeの内容を参考にしながら副作用について整理していくことにする。

例えば、RubyのArray#popメソッドは副作用を持つ関数と言える

array = [1, [2, 3], 4]
p array.pop      # => 4
p array.pop      # => [2, 3]
p array          # => [1]

array.popによって、arrayに処理後の状態が残る。この残された状態が副作用。

副作用を避けるべき理由

副作用は「ソフトウェアに複雑さをもたらすから」。

具体的には、時間的結合(temporal coupling)と呼ばれる結合につながる。副作用を持つコードは、前後のコード間に「状態の変化」を生み出し、その状態の変化に依存する形でコードの実行順序を保証する必要が出る。

例: セマフォは取得してからでないと解放できない。

取得 → 解放 という処理の前提が生まれてしまう。

時間的結合により順番に依存することで、システム全体の挙動の理解やデバッグを難しくする。 局所的な順番依存がシステム全体の複雑性を上げ、不安定な状態につながってしまう。

副作用を持つ関数の特徴

以下のようなペア(片方は副作用を作成し、もう片方はそれを取り消す)を持つことが多い。

副作用を避ける方法

関数型言語を使用する

関数型言語の副作用の管理には役立つが、副作用を完全になくすことはできないことを頭に置いておくこと。 例えば、OSレベルの操作(ファイルI/Oなど)には必ず副作用が伴う。

オブジェクト指向言語を適切に活用する

以下は、副作用を局所化して隠蔽するコードの例。

class LineCounter
	class << self
		def count(file_name)
			initialize_state(file_name)
			count_the_lines
			finalize
		end

		private

		def initialize_state(file_name)
			@n_lines = 0
			@file = File.open(file_name)
		end

		def count_the_lines
			@file.each_line do |_line|
				@n_lines += 1
			end
		end

		def finalize
			@file.close
			@n_lines
		end
	end
end