参照透過性ってなんだっけ?
ふと「参照透過性」てなんだっけ?と気になったので、色々考えたことをまとめてみる。
参照透過性を調べると「式をその値に置き換えてもプログラムの意味が変わらないこと」と出てくる。
参照透過性がある例はこんな感じ。
fun add(x: Int, y: Int): Int {
return x + y
}
val sum = add(2, 3)
// これはadd(2, 3)の戻り値5に置き換えても意味は同じ
val sum = 5
そんなに特別なことじゃなくない?と思ったけど、参照透過性を満たさない例は普通にある。
例えば標準出力。
val x = println("Hello")
Kotlinのprintln
の戻り値はUnit
だけど、
val x = Unit
とは明らかに意味が異なる。
他にも、内部の状態を変更するような関数は参照透過性を持たない。
class Counter() {
var i = 0
fun increment() {
i++
return i
}
}
この increment()
という関数は
val counter = Counter()
val x = counter.increment() // x = 1
val y = counter.increment() // y = 2
だが、
val counter = Counter()
val x = 1 // 右辺を値に置き換えると
val y = counter.increment() // yの値が1に変わった!
明らかに意味が変わってしまう。
「副作用」が関係するらしい
関数の戻り値を返す以外の、状態を変化させるような操作を言う。
例えば、グローバル変数の値を書き換える、DBやファイルへの永続化、標準出力などが挙げられる。
また、関数の外部の状態を受け取ることも副作用と呼ぶようで、DBやファイルの読み取りも副作用である。また、乱数や現在時刻など、実行結果が都度変わってしまうようなものも副作用に含まれる。
副作用を持つような関数は参照透過にはならない。
参照透過性と副作用の概念は微妙に異なるはずだが、よくわからない。
冪等性とは違うんだっけ?
冪等性とは、同じ処理を何度繰り返しても結果や状態が変化しないこと。
HTTPメソッドのPUT
とPATCH
の違いとかで有名。
参照透過性を持つ関数は当然冪等性を持つ。
val x = someFunction()
val y = someFunction()
//もし x!=y ならば sumFunctionは副作用を持っている。
ただし、冪等性を持つ関数が参照透過性を持つとは限らない。
hoge.setValue(42)
hoge.setValue(42)
hoge.saeValue(42)
... // 何度繰り返しても hoge.value == 42
だが、hoge
の内部の状態を変更しており、参照透過性はない。
そもそも戻り値がUnitの関数は参照透過性を持ち得ない
println()
もsetterも、戻り値がUnitである。
このような(実質)何も返さない関数は、何かしらの副作用を持つ処理を行っているはずで、そうなると参照透過性は持たない。
厳密には
fun doNothing() {}
という関数は参照透過性を持つが、何もしない関数を呼び出すことに通常意味はないので、戻り値がUnitの関数は参照透過性を持たないと考えてよい。
オブジェクト指向のメソッドは?
ところで、Kotlinはオブジェクト指向の言語である。クラスに紐づくメソッドの扱いはどうなるんだろうか?
例えば、単純なgetterを見てみると
hoge.getValue() // 仮に111になるとする
hoge.setValue(222)
hoge.getValue() // ここでは222
hoge.getValue()
の値が変わっているので、参照透過性を持たないということになるのだろうか...?
さらに、クラスを引数に持つ場合はどうなるか?
class Point(val x, val y)
こういうクラスを引数に持つような関数
distanceFromOrigin(p)
を考える。これはどうなるか?
Point
というクラスをValue Objectとして設計しておけば、参照透過性を持つと言っていいきがす。
そもそも、オブジェクト指向は「(隠蔽された)内部状態を持つクラスの振る舞い」を基に記述するし、対して関数型プログラミングは「(数学の関数のように)同じ引数に対して常に同じ戻り値を返す関数」を中心とするというような違いがある。
思想が異なるものを無理に当てはめようとしているので、わかりづらくなっている面がありそう。
まとめ
「参照透過性」として「式が値に置き換えられる」と言う意味は大きく分けて2つありそう。
1つ目が、「戻り値の値以外、関数が意味のある処理を行なっていない」こと。
2つ目が、「同じ引数に対して、必ず(外部の状態などに依存せず)同じ値を返す」こと。
副作用とか純関数などの概念と関わりがあるので、きちんと改めて関数型プログラミングを勉強してみようと思った。