はじめまして!!標準体重 65kg 目指して増量中のそうまです!!
今日は参照透明性について疑問に思ったことと、JSで何故副作用が起こるかを個人的に調査したので殴り書きしていきます!!!
調査のきっかけとなったある勘違い
関数型言語における参照透明性って、変数が作られるたびにオブジェクトだろうが値だろうが毎回ディープコピーされてメモリに格納されるものだと思っていました。
(シャローコピーという概念が存在し無い)
だから副作用とは無縁で安全だと。
でも、それは完全な勘違いでした。
そもそもシャローコピーとは
シャローコピーとは、変数に“値そのもの”ではなく、“メモリの番地”がコピーされることを指します。
たとえば、次のようなコードがあるとします。
const person = { name: "taro", age: 12 }
const target = { ...person }
このとき、target
に格納されるのは person
の中身ではなく、格納されるのは、person
が保持しているオブジェクトのメモリ番地です。
つまり、target
も person
と同じヒープ上のオブジェクトを指している状態になります。
これが、シャローコピーと呼ばれるものです。
一方で、次のような場合はどうなるでしょう?
const person = { name: "taro", age: 12 }
const target = person.name
この場合、target
に格納されるのは "taro" という文字列そのもの(=値)であり、 person.name
が参照していたメモリとは無関係なスタック上のコピーになります。
この違いが、シャローコピーと値コピーの本質的な差です。
ディープコピー
メモリの番地ではなく、オブジェクトの値そのものをそのままコピーする
なぜオブジェクトがシャローコピーされるのか?
それは、可変長のデータ構造だからです。
オブジェクトの中身は、構造も違えば深さも違う。
さらに、途中には再帰的な参照や構造が含まれることも多く、全体のサイズを静的に割り出すのは現実的ではありません。
もし、メモリ確保時点でオブジェクト構造のすべてを調べてサイズを決めようとしたら、それだけで膨大な処理となってしまいます。
だからこそ、オブジェクトは実行時に動的にメモリ確保できる「ヒープ領域」に乗ります。
そして、そんな可変長のオブジェクトを毎回ディープコピーしていたら、メモリ消費も処理時間も爆増してしまいます。
そこで採用されているのが、シャローコピー です。つまり、オブジェクトの“メモリ番地(参照)”だけをコピーすることで、メモリ効率を保っているのです。
一方で、プリミティブ型はシンプルです。
型さえ決まっていれば、最大で必要なメモリサイズも静的に決める事ができます。
だから、値をそのままコピーして渡しても問題はありません。
そのため、コンパイル時に静的にメモリを確保する領域であるスタック上で完結するようになっています。
ヒープとスタック領域って何?
まずヒープ領域とは、サイズが一概に決められない可変長データを格納するためのメモリ領域です。
実行時に必要なサイズを判断して、値をメモリ上に格納します。
一方、スタック領域は、あらかじめサイズが決まっているデータを扱います。
例えばプリミティブ型のように、型さえわかれば最大で必要なサイズが静的に確定するようなものです。
そういったデータは、コンパイル時にメモリの配置を決める事が出来ます。
参照透明性のある関数型言語だと、「でも、副作用がないのでコンパイル時に決めれるやん」ってなりますが、遅延評価により実行時に決めているらしいです (※ ここはまだ勉強中です)
GC(ガベージコレクタ)の重要性
ヒープに割り当てられる値は、大きくなりがちです。
だから上記で説明したように、毎回コピーするのではなくメモリの番地(参照)を使って効率的に扱っています。
ですが――
参照をしているが故に、「どこで使用されているか」「いつ使われなくなったか」の判断は簡単ではありません。
参照されていないものを、即座にメモリから解放できなければ永遠にメモリを蝕み続けるだけです。
プリミィティブ型であれば判断出来ます、何故ならスタック領域に格納されており参照されることはないからです。
もし仮にオブジェクトの値として代入されていても、値をそのままコピーして渡すだけでそのまま役目を終えるため、必要なくなったとして自然とメモリ解放の判断が出来ます。
そこで登場するのが、ガベージコレクタ(GC) です。
GCは、プログラムの中でまだ使われていそうな変数を起点にそこから関係のあるデータをたどっていきます。
そして、どこからも参照されていないデータを見つけたら「もう使われないな」と判断してヒープからメモリを解放します。
纏め
長くなってしまいましたが、最後に参照透明性とはなんであるかを話すと
参照透明性とは、同じ入力があれば常に同じ出力を返すという性質の事を言います。
裏を返せば、変数の状態や副作用によって結果が変わらないということです。
それは、値が不変(イミュータブル)であり、オブジェクトの中身を破壊的に変更出来ないからこそ実現出来ます。
つまり、ヒープにあっても「壊せない」から副作用が起こりません、これが参照透明性の強みです。
一方でJSはどうでしょうか....オブジェクトであるのに好き勝手変更出来てしまいます、つまり副作用の聖地なのです。
なので必要に応じて気を使いバグが起こらないように「参照されてるかもだから、念のためディープコピーしとくか」
と、余計な思考コストを払う羽目になります。
そして、こういう“気遣い”がバグの温床になるんだと考えています。