読者です 読者をやめる 読者になる 読者になる

「オブジェクトをイミュータブルにしろ」って言うけど、それってたとえば状態が変わったらオブジェクト作り直すってことでしょ、ちょう非効率じゃん。って思ってたんだけど、

オブジェクトの内部の値がイミュータブルであれば、今後もその値は変更されないことが保証されているので、新しい状態を持った新しいオブジェクトの内部の値のうち、変更のない部分(つまり値のうちのほとんど)は古いオブジェクトの値をそのまま参照すればよく、コピーする必要がないということを @takkkun が言っていて(正確には、イミュータブルなリストに新しい値を追加した新しいリストを作るときには、中身をコピーする必要ない。変更されないことが保証されてるから、という話だった)目から鱗が落ちたのでここに記して置こうとおもった。

で終わろうと思ったんだけど、もう少しちゃんと書く。

ミュータブルな世界では同一性の問題がある。

たとえば playerA と playerB の HP がたまたまおなじ 10 であったとしても playerA と playerB の HP 変数が同じ数値オブジェクトを参照していたらまずい。これは当然で、同じ数値オブジェクトを参照していたら playerA の HP が 3 減ったときに playerB の HP も同じく減ってしまう。playerA と playerB は同一のオブジェクトではないのに、同じものを参照していたらまずい、ということだ。

こういう問題があるので、「A と B 、別のオブジェクトはそれぞれの内部状態を別のメモリアドレスに保持しなければならない」というのがミュータブルな世界における常識、基本だと思う。そうしないとどこで変わっちゃうかわかんないからね。

でもイミュータブルなら心配ないよ!

そういうミュータブルな世界に慣れきってしまっていたわたしは「イミュータブルってことは状態が変わったらオブジェクト作り直すとか、毎回コピーが発生してちょう馬鹿じゃん」って思ってたんだけど、冒頭に書いたようにそうではない。

すべてがイミュータブルな世界では、playerA の HPが減ったら playerA を 作り直さないといけなくなるんだけど、ここで「新 playerA」と「旧 playerA」が生まれる。

「旧 playerA」と「新 playerA」が別々のオブジェクトなので、ミュータブルな世界の常識だと、旧の内部状態からHPだけを書き換えてまるっとコピーして「新playerA」を作らないとね、って話になるんだけど、イミュータブルな世界では、あるメモリアドレスに置いてある値は決してそのあと変わらないことが保証されているので、新 playerA を作るためにわざわざ「別のメモリアドレスに旧の値を全部コピー」なんてしなくていい。新 playerA の内部状態のうちほとんどは旧 playerA と同じメモリアドレスを参照するようにして、HP だけは別のメモリアドレスに作った新しい値を参照するようにすればよい。

こうすれば、例えばいくら大きな HashMap であろうといくら大きな List であろうと、「値を追加した新しい Map や List を作りたい」ってときに「全部コピーして作り直す」必要がなくなる。

これって、わかってしまいえば当たり前の話なんだけど、ミュータブルな世界の常識にとらわれているわたしが持っている「イミュータブルって言ってもさー、これどうすんのよ疑問」の、大きなもののうちのひとつが氷解して「エウレーカ!」ってなったので書いた。

以下2016.2.17 追記

ほぼ3年前の記事が急に伸びてびっくりしています。その中で id:sona-zip さんのブコメを見て「あー追記したほうがいいかも」っておもう部分があったので追記します。

「オブジェクトをイミュータブルにしろ」って言うけど、それってたとえば状態が変わったらオブジェクト作り直すってことでしょ、ちょう非効率じゃん。って思ってたんだけど、 - life.shou

理屈はわかったけど実践的にはまだ理解できてない 値変更する度に新たなオブジェクト作って参照先を変更(アドレスのコピーは発生する) 旧オブジェクトを参照しているところをすべて新オブジェクトに直すって現実的?

2016/02/17 07:45
b.hatena.ne.jp

たとえば Scala の場合、case classにcopyというメソッドが生えていて、これは指定した引数の内容は書き換えてあとは同じ値を指す新しいオブジェクトを作るという、まさに上述のような動きをするメソッドです。それを利用することで、たとえば上記のような「ダメージ食らったらダメージ分だけhpの減った新しいオブジェクト作る」というのは以下のように書くことができます。

case class Player(name: String, hp: Int, mp: Int) {
  def damaged(damage: Int): Player = copy(hp = this.hp - damage)
} 

ここまでは事実とか情報の類で、以下は私の意見です。

ところで、オブジェクトは「常に」イミュータブルにすべきなのでしょうか? 答えは「It depends.」な気もします。たとえばライブラリなどがミュータブルなデータ構造が当たり前の世界で、すべての自作クラスをイミュータブルにする必要はないと私は考えます。一方で、複数のスレッドから触られることが多くミュータブルであることが怖い事故を引き起こしそうだなー、とか、そういう場合には、わざわざ自前でcopyメソッド的なものを実装してしまう、という選択肢も「アリ」なんじゃないかなと思います。

かように、言語そのものや文化そのものがイミュータブルを推奨しているのではない場合、上記の内容は「イミュータブルにしたいときが出てきたら知っておくともしかしたら役に立つかもね」くらいの内容かなと思いますので、ミュータブルが当たりまえの世界でもすべてをイミュータブルにする必要はないんじゃないかなと私は思います。