はじめに
paizaのプログラミングスキルチェックを解いていて、変数の代入について詰まった箇所を深掘りしたところ、ミュータブルとイミュータブルを理解する必要があったので記事にしました。
言語によって異なるそうですが、今回はRubyの場合を想定した深掘りになります。
以下は私が詰まったところの内容です。
a, b, curry, riceには何がはいるでしょうか?
a = b = 0
a += 1
p a # a => ?????
p b # b => ?????
curry = rice = []
curry << "カレー"
p curry # curry => ?????
p rice # rice => ?????
答え
a => 1
b => 0
curry => ["カレー"]
rice => ["カレー"]
どちらも2つの変数に同じ値を持たせたはずなのに、bは値が変わらず、riceは値が変わったように見えました。 この違いの正体が今回のテーマです。
なぜ一方だけ値が変わったように見えるのか
結論:a += 1とcurry << "カレー"は、見た目は似ていても「やっていること」が違うから
具体的には、a += 1は変数に別のオブジェクトを代入し直す操作、curry << "カレー"は同じオブジェクトの中身を書き換える操作です。
そして、この「中身を書き換える」操作ができるオブジェクトのことを、ミュータブルなオブジェクトと呼びます。ここが今回のタイトルにつながる部分です。
この違いを理解するために、まず「変数とは何か」から順に見ていきます。
その前に:オブジェクトとは何か
まず前提として、Rubyでは扱うデータがすべてオブジェクトです。
整数の0,1、文字列の"カレー"、配列の[1, 2]など
これらは全てオブジェクトといいます。
これらオブジェクトは変数に代入できます。
ただし、「代入」で実際に何が起きているのかが、今回の問題のポイントになります。
変数は 「値の箱」 ではなく 「オブジェクトの場所を示すメモ」
変数は「値を入れる箱」とイメージすると思います。
最初のうちは分かりやすいですが、もっと実態に近いのは次のようなイメージです。
オブジェクトは、どこかの場所に置かれている本体。変数は、その本体がどこの場所にあるか指し示すメモ。
例えば、次のコードは
curry = ["カレー", "ライス"]
「curryという箱に["カレー", "ライス"]が入っている」のではなく、
「どこかの場所に["カレー", "ライス"]が置かれていて、curryはそのどこかという場所を示すメモを持っている」というようなイメージです。
このイメージが効いてくるのが、変数から変数への代入です。
curry = ["カレー", "ライス"]
rice = curry
p curry # => ["カレー", "ライス"]
p rice # => ["カレー", "ライス"]
rice = curryは「curryの["カレー", "ライス"]をコピーしてriceという箱に入れている」のではなく、
「curryが持っている場所を示すメモを、riceにも書き写している」だけです。
結果として、riceとcurryは同じ1つの場所を指す2枚のメモになります。
オブジェクトの本体には、1つ1つに固有の識別番号が割り振られています。2枚のメモが同じ本体を指しているかは、この番号で確かめられます。
その方法として、object_idメソッドがあります。
curry = ["カレー", "ライス"]
rice = curry
p curry.object_id # => 1120
p rice.object_id # => 1120(curryと同じオブジェクトID(識別番号)を持っている)
識別番号が同じということは、2枚のメモが同じ場所を指している証拠です。(番号の具体的な数値は実行環境によって異なります)
同じ本体の場所を2枚のメモで指しているとき、片方から本体に手を加えた場合どうなるでしょうか。
ここで、冒頭の2つの操作の違いが効いてきます。
「代入し直す」か「本体を書き換える」か
冒頭のa += 1とcurry << "カレー"は、変数に対してやっていることが根本的に違います。ここが今回の謎の核心です。
<< は「本体を書き換える」——もう一方のメモにも伝わる
まずcurry << "カレー"から見ます。<<は変数に代入しません。curryが指すオブジェクトはそのままで、そのオブジェクト自身の中身を書き換えます。
curry = ["カレー", "ライス"]
p curry.object_id # => 30 (["カレー", "ライス"]のオブジェクトID(識別番号))
rice = curry
curry << "豚カツ"
p curry # curry => ["カレー", "ライス", "豚カツ"]
p rice # rice => ["カレー", "ライス", "豚カツ"](curryと同じ!)
p curry.object_id # => 30(中身を変えてもIDは同じ = 同じ本体を書き換えた証拠)
p rice.object_id # => 30(curryと同じオブジェクトID(識別番号)を持っている)
注目してほしいのは、2枚のメモは一切書き換えていないことです。riceもcurryも同じ場所を指したまま、「本体」だけが変わりました。
object_idが前後で変わっていないのが、その証拠です。同じ本体を書き換えただけなので、指し示す場所(ID)は変わりません。
だから同じ本体を指すもう一方のメモ(rice)からも、"豚カツ"が追加された姿が見えます。
+=は「別の本体を代入し直す」——もう一方のメモには伝わらない
次にa += 1。これはa = a + 1の略で、変数aに新しいオブジェクトを代入し直しています。これを自己代入演算子と呼びます。
だからaが指す先が、別の本体に変わります。
a = 0
p a.object_id # => 1(0のオブジェクトID(識別番号))
b = a
a += 1
p a # a => 1
p b # b => 0
p a.object_id # => 3(a += 1により、aが別の本体(1)を指すように変わった)
p b.object_id # => 1(bは元の0を差したまま。無傷)
a += 1は「0という本体を書き換える」操作ではありません。
0に1を足した結果である1という別の本体を、aに代入し直す操作です。aのメモが1の場所に書き換わる一方で、bのメモは元の0を指したままなので、無傷です。
大事な注意:これは「配列だから/整数だから」ではない
私自身勘違いしていました。
ここで勘違いしやすいのですが、riceとbで結果が別れたのは「配列だから」「整数だから」ではありません。<<(本体を書き換える)を使ったか、+=(代入し直す)を使ったかという、操作の違いが原因です。
証拠に、同じ配列でも+=を使うと、もう一方のメモは変わりません。
curry = rice = ["カレー", "ライス"]
curry += ["豚カツ"]
p curry # => ["カレー", "ライス", "豚カツ"]
p rice # => ["カレー", "ライス"]
curry += ["豚カツ"]はcurry = curry + ["豚カツ"]の略で、新しい配列を作ってcurryに代入し直しています。
riceは元の配列を指したまま無傷です。冒頭のa += 1とまったく同じことが、配列でも起きています。
では、ミュータブルとイミュータブルは何のためにあるのか
「差の原因が操作の違いなら、ミュータブルとイミュータブルは関係ないの?」と思うかもしれません。ですが、しっかり関係しています。
両者の言葉を言い換えると以下になります。
- ミュータブル = 一度できたオブジェクトの中身を、あとから変更できる
- イミュータブル = 一度できたオブジェクトの中身を、あとから変更できない
この2種類は、オブジェクトのクラス(種類)によって決まっています。
- ミュータブル = 配列(Array)、ハッシュ(Hash)、文字列(String)など
- イミュータブル = 整数(Integer)、シンボル(Symbol)など
ここで効いてくるのが、さきほどの<<のような「本体を書き換える操作」です。本体を書き換える操作ができるのは、ミュータブルなオブジェクトだけです。配列は<<で中身を書き換えられますが、整数には中身を書き換える手段がありません。
a = 0
# a << 1 のような「0という本体を書き換える」操作は存在しない
a += 1 # できるのは「別の本体を代入し直す」ことだけ
だから、「curryをいじったらriceまで変わった」という一見びっくりする現象は、ミュータブルなオブジェクトを共有しているときにだけ起こりえます。イミュータブルな整数は、そもそも本体を書き換える操作がないので、こういうことは起こしようがありません。
まとめ
ミュータブルとイミュータブルを整理します。
| 種類 | 性質 | 代表例 |
|---|---|---|
| ミュータブル | 中身を変更できる | 配列、ハッシュ、文字列 |
| イミュータブル | 中身を変更できない | 整数、シンボル、true/false、nil |
- 変数は値を入れる箱ではなく、オブジェクトがどこにあるか指し示すメモ
-
<<のように本体を書き換える操作をすると、同じ本体を指すもう一方のメモにも伝わる -
+=のように別の本体を代入し直す操作は、指す先が変わるだけなので、もう一方のメモには伝わらない - 本体を書き換える操作ができるのはミュータブルだけ。
(補足)
-
mutable(ミュータブル)は「変わりやすい、変更可能な」という意味 -
immutable(イミュータブル)は「変わらない」という意味でim-の打ち消しがついている。 - オブジェクトが変更禁止かどうかを返す
frozen?メソッドがある
"カレー".frozen? # => false(変更可能)
1.frozen? # => true(整数は常にfrozen(凍結済み))
- 2つが同一のオブジェクトかを判定する
equal?メソッドがある。==との違いとして==は「中身が同じか」、equal?は「同一オブジェクトか」
curry = ["カレー"]
rice = ["カレー"] # 中身は同じだが、別々に作っている
# == は「中身が同じか」を見る
p curry == rice # => true(どちらも["カレー"]で中身は同じ)
# equal? は 「同一の本体か」を見る
p curry.equal?(rice) # => false(別の場所に作った別の本体)
p curry.object_id # => 30
p rice.object_id # => 40(object_idが違う = 別の本体)