大事なことは全部MLが教えてくれた 〜 Apple の Swift の mutability 周りの件を理解する
開発者アカウントに金が出せない貧乏人の方々が、次の Apple の Swift のコードの挙動がわからない、というので盛り上がっております:
let a = [1,2] // a = [1,2] var b = a; // b = [1,2] b[1] = 3; // a = [1,3] b = [1,3] b.append(5); // a = [1,3] b = [1,3,5] b[1] = 4; // a = [1,3] b = [1,4,5]
もちろんわたしも貧乏ですからわかりやすい炎上案件を待っておるわけです。これはわかりやすいわからないが来たね。
だいたい
- b[1] = 3 とやると a[1] も変化する、これがわからないという人
- b[1] = 4 とやると a[1] が変化しない、これがわからないという人
二種類いるようです。私はまず、 b[1] に代入できることがわかりませんでしたのでそれ以前の人間です。
こういうとき、このコードが何をやっているかを理解するには同じ挙動をする ML のコードを書くとわかりやすいです。なぜ ML かというと、もちろんみんなの好きな関数型言語であって、かつ代入などの副作用を持っていて、でも基本は不変データなので可変データの部分はちゃんと明示してやらなきゃいけない言語なので、こういう副作用でわけわからんコードを読み解くには明示する分都合がよろしいからです。もちろんみんなが好きな関数型言語というのはここではとくに意味がありません。Haskell は本題に行く前に Haskell は純粋で純粋なのに副作用があったりなかったりうだうだモナドがああだこうだで圏論があしたどしたでドヤってめんどくさ過ぎるのでこういうのには向きません。
ええっと、断っておきますが、これは上の Apple の Swift コードの挙動と同じ挙動を持つ一番単純な ML のコードって何かしらん、という試みです。オッカムの剃刀の原理を信じれば、そのコードと同じことがおそらく Apple の Swift の中で起こっておるであろう、ということです。実際の Apple の Swift の中がどうなっているかは私貧乏ですから知りません。ただ、まあ ML のコードを見れば、まあ多かれ少なかれ、こんな内部実装なんであろうなあ、と十分推測できるわけです。
そんな手探りせずに仕様書読めという突っ込みもありですがね。まあこういう群貧撫AppleのSwiftみたいな話もたまにはいいんじゃないですか、仕様書探すの難しいらしいし (http://cpplover.blogspot.jp/2014/06/appleswift.html)、どんなプログラミング言語であれ、仕様書を読まないとわからない言語仕様というのはできるだけ少ないに越したことはありません。
で、書くとこんなんになります。 http://try.ocamlpro.com/ で一行づつ入れながら、途中で a;; とか b;; して変化を見ながら試してみるといいよ:
let a = [| 1; 2 |];; let b = ref a;; (* 明示的に参照ですって言わないといけないんです *) !b.(1) <- 3;; (* 参照の中身を見るには !b って書かないといけないんです *) b := Array.append !b [|5|];; (* 参照先を変えたいときは b := ほげほげ って書くんです *) !b.(1) <- 4;; (* 配列のn番目要素は a.(n) て書くんですキモイ *)
- b[1] = 3 とやると a[1] が変化するのがわからない人は、 var b = a がコピーは行わないってことをわからない人です
- b[1] = 4 とやると a[1] が変化しないのがわからない人は、b.append(5) が b 自体を変えてしまうことがわからない人です、ってかこれ普通わかんないでしょ
でもこれ両方とも ML で書いたコードではハッキリわかりますよね。少なくともこの例を見る限りは、
- var b = e は e の値への参照を作るだけでコピーはしない
- b.append(5) は参照する値に 5 をくっつける。配列だから結果はコピーになる。 b はそこに参照先を変える
という意味だとわかる。(Array.append はコピーするんですよ。というか配列の後ろに何かつけてもコピーじゃないってそんなすごい配列があったらそれは配列じゃない) copy-on-write とかカッコイイ機能を入れようとしたけどバグっているんだ!とかそういうオシャレ事案ではない。
クソですね。
オブジェクトのメソッド呼び出しのような外見なんだけど、呼ばれた後、オブジェクト自身は他のに摩り替わっている。高度なナリスマシ事案だ。これはヒドイ仕様だと思います。 ML なら b := Array.append !b [|5|] と書くわけなので、 b の参照先が変わっていることがわかる。append すると配列がコピーされるかどうかはもちろん配列というデータ構造の特性を知らないといけないのであれだけど。
せっかくだから Apple の Swift の人の立場になって考えましょう。これはどう直せばいいですかね。
- var b = e は e の値のコピーを作ってそれを参照することにする
これはパフォーマンス落ちる…のなら、気休めに copy-on-write 導入しますか。 b[1] = 3 とやっても a[1] が変わらない… まあよく判んない人相手にはこの挙動が一番いいのかもしれませんね。
- 配列を頭よくして b.append(5) とやっても b の中身は成りすましにならず、ちゃんと b[1] = 4 の後 a[1] = 4 になる。
これは配列はもはや配列ではなくて何か別の rope みたいなものになります。うーんそれは配列ではない。
- b.append(5) などというクソいメソッドはやめて b = b ++ [5] みたいにする。
これならコピーしていることがわかる?かな?いやー無理でしょうねぇ。でも少なくとも現時点での挙動でメソッド呼び出しの形は誤解をまねきやすすぎるよね。
この中でどれが一番いいか…やはり ML を使うことだと思いますね。おわり。