Rubyの「=」について無限に紐解いてみる

はじめまして、Baseconnectエンジニアの御園と申します。 私は2022年の4月1日にBaseconnectにエンジニアとしてJOIN、実務として経験するのは初めてのRubyを使って開発業務を行うことになりました。

そして、入社してから1ヶ月にも満たない4月22日、無茶振りによって 社内向けにLTをすることになり、私はプログラミング言語オタクとしての本領を発揮すべく、Rubyにまつわる些細な疑問を深堀りしたLTを行いました。

結果として割と反応はよく、社内の皆さんに良い感想を沢山頂けたので、今回はそのLTの中から抜粋し、テックブログとして記事を書かせてもらうことになりました。どうぞ、よろしくお願いいたします。

introduction

さて、皆さんがRubyを学ぶ時一番最初に学んだ事を覚えていますか? Rubyでプログラムを書く準備、Rubyという言語について……様々な事を学んだでしょう。 そして、Rubyイカしたプログラムを書く時、誰もが最初に学ぶものがあります。 そう、それは変数に値を格納するための「=」ですね。

プログラマが一生付き合うことになっていく「=」。それを記述した数はもはや数え切れないでしょう。 その「=」がもつ働きは、実はRubyという言語の性質を追求するのに実はもってこいという事を知っていましたか? 一回立ち止まって、あるいは少し後ろを向いてみて、「=」にもう一度目を向けてみましょう。 さぁ、私と一緒にRubyの「=」について追求する旅に、一緒に出かけてみましょう。

motivation

一番最初の些細な疑問は、私がRubyでとある演算子を使おうと思ったときに生まれました。 以下のようなコードです。

value++

++ はインクリメント演算子と呼ばれ、JavaScriptPHPなど、Cに影響を受けた色々な言語で使うことが出来ます。 意味は単純で、"変数の値を1加算する"と言ったものです。よく for と一緒に使用されたりしますね。例えばC++のコードでは以下の様に使えます。

for (int i = 0; i < 10; i++) {
  // iは0〜9まで、1ずつ加算されながら、このブロックの中の処理は反復される
}

さて、Rubyの世界に戻りましょう。Rubyに堪能な読者の人ならば、既に「そんなもの、Rubyには無いよ」と鼻息を荒くしていることでしょう。 まさしくその通りです。Rubyには ++ が定義されていません。なので、私は残酷にもRubyにエラーを吐かれてしまいました。 ( ++を使った直後の行を対象に以下のエラーを出される )。

syntax error 

Rubyにはインクリメント演算子も、"変数の値を1減算する"ところのデクリメント演算子 -- もありません。 例えば1加算する、といった事を実現するためには、以下の様にする必要があります。

value = value + 1

ただし殆どの人がこう書かず、必ずこう書くでしょう。

value += 1

自己代入などと表現されるこの += は、 value = value + 1 と同じ意味を持っています。 これはある意味シンプルです。 変数の値を加算、あるいは減算するという事に対しては、全て同じ演算子を使えば良いということになります。 1を足す時も、何かそれ以外を足す時も+=。1引く時も、何かそれ以外を引く時も -=。 わざわざ ++-- という特別な演算子の事を学習する必要がありません。 しかし、JavaScriptPHPなどに見られるように、”あっても良さそう”という気持ちが生まれるのも確かです。

何故Rubyにはインクリメント、デクリメント演算子が無いのでしょう? プログラマは探求をするのが好きです。私もご多分に漏れずその性を持っていますから、こんなちょっとした興味で、Rubyにこれら2つの演算子が無いことを調べ始めました。 そして、すぐさま、その疑問が解消される事については、Rubyという言語そのものの性質を知らなければならないことに気づくのでした。

奇妙なコード

Rubyを学習しようと文献を漁ると、高確率で目にしたり、聞いたりするものがあります。

Rubyでは全てがオブジェクト

これはどういう事なのでしょうか? この事を知るために、まずは次のようなシンプルなコードを実行してみます。

class Hoge
end

i = Hoge.new
puts i.object_id

object_id メソッドは、そのオブジェクトのIDを取得する事が出来ます。 Hoge クラスは new メソッドによってオブジェクトとして実体化され、 i に格納されました。 そして、その i に対して object_idメソッドを使用することによって、実体化したオブジェクトのオブジェクトIDが取得出来るというわけです。 例えば私の環境では、以下のように出力されました。

60

では、もう一つ、次のようなコードではどの様に出力されるでしょうか?

class Hoge
end

puts Hoge.new.object_id
puts Hoge.new.object_id
puts Hoge.new.object_id

このコードは、例えば私の環境では以下のように出力されます。

60
80
100

おそらくどのような人の環境でも、3つとも違うオブジェクトIDが出力されるでしょう。 通常、Rubyではオブジェクトが作成された時、他のオブジェクトと衝突しないようにユニークなオブジェクトIDが割り振られます。 上記のコードでは、3回 Hoge クラスのオブジェクトの作成をしており、それぞれユニークなオブジェクトIDが割り振られた結果、3回とも違うオブジェクトIDが出力されたという事になっています。

今、私達は object_id でそのオブジェクトが持つオブジェクトIDを知る事が出来る事を理解しました。 では、次のようなコードはどうでしょうか?

puts 20.object_id

一見これはとても奇妙です。 もし、これが奇妙に見えない人がいたら、ニヤニヤしながらこの先に進んでください。 しかし、JavaPHP、他のオブジェクト指向の特性を持つプログラミング言語をやったことのある人であればあるほど、このコードがとても奇妙であるということは共感できるはずです。 そして、Rubyではこのコードは問題なく解釈でき、例えば次のような出力を生むはずです。

41

なんと 20 という数字に対して、 object_id メソッドが使えてしまいました。 Rubyでは全てがオブジェクト という言葉がここで脳裏を反芻します。 もう一度先のコードを見てみましょう。

puts 20.object_id

このコードは何が奇妙なのでしょうか? 先程 object_id メソッドは、オブジェクトのオブジェクトIDを取得できるものという説明をしました。 その「オブジェクト」とは、潜在的に「new メソッドを使ってクラスを実体化したもの」という印象があったはずです。ですが、このコードでは new メソッドはおろか、クラスの定義すら出てきません。

プログラマ、特にRubyをやったことのないプログラマ達にとっては、20という数字は「ただの20という数字」でしかありません。それ以上でもそれ以下でも無く、20という数学的性質を表す分量以外の何者でも無いのです。 しかし、Rubyでは、それに対して直接 object_id を使うことが出来てしまいました。 一体何が起きているのでしょうか?

全てがオブジェクトということ

Rubyのリファレンスマニュアルを参照すると、オブジェクト の欄に以下のような記述が出てきます。

オブジェクトとは Ruby で扱える全ての値はオブジェクトです

これはとても興味深いことです。 この事をあえて、他の言語で擬似的に表現することにしてみましょう。 例えば、

hoge = 42

このようなRubyのコードがあったとして、C++では通常このコードと同じような事をするならば

int hoge = 42;

となるはずです。しかし、Rubyでは 扱える全ての値はオブジェクト ということでした。 つまり、実際にRubyで起こっていること を忠実に再現するならば、C++では以下のようなコードになると言えます。

auto hoge = new Integer(42);

42という数字を代入するために、Integer クラスのオブジェクトを42という数字で初期化し、それを変数に代入しています。 これが意味するのは、プログラム中に記述された

= 42

というものが、C++Rubyで決定的に違うということです。 この事を踏まえると、先程起きた奇妙な出来事について、紐解いていけそうです。 もう一度コードを見てみましょう。

puts 20.object_id

直感的に言えば、これはC++ではこのようなコードになりそうです。

std::cout << 20.object_id();

しかし、このコードは大方の予想通り通りません。20 という数字そのものに対して、 object_id などというメソッドは無いからです。 20はあくまで20という数字リテラルでしかなく、それ以上でもそれ以下でも無いのです。 ですが、 実際にRubyで起っていること をまたここでも忠実に再現してみましょう。

std::cout << (new Integer(20))->object_id();

これなら通りそうです。とどのつまり、Integer クラスのオブジェクトを20で初期化したあと、 object_id というメソッドをそのオブジェクトに対して呼び出し処理をしています。

Rubyでの 全てがオブジェクト という事がここまでで具体的に見えてきました。 実際にRubyのリファレンスマニュアルを見ると、Integer という整数クラスの存在を確認する事が出来ます。 その継承を追ってみると

Integer < Numeric < Comparable < Object < Kernel < Basic Object

となっており、これのうち Object クラスに object_id メソッドが定義されていることが確認できます。 Rubyでは、ただの数字に見えても、実際には Integer クラスのオブジェクトであり、それに付随して実装されているメソッドをダイレクトに呼び出せたという訳です。

これはRubyの大事な性質です。何気なくタイピングしている数字、文字列、それらはRubyの中では全てオブジェクトとして顕在化するということに留意しなければなりません。 しかし、この性質を意識していると、奇妙なコードも徐々に"Rubyでは当たり前"という見方に変わっていくでしょう。

もっと踏み込んでいく 〜Integerクラスの工夫〜

ところで、ここで一つ疑問が生まれます。 全てがオブジェクトなのであれば、

・数値を宣言する毎にオブジェクトが生み出されている ・同じ数字を宣言したとしてもオブジェクトが生み出され、パフォーマンスが落ちるのではないか?

という疑問です。先程、Rubyでの次のようなコードを

hoge = 42

C++で擬似的に表すのに次のようなコードを用いました。

auto hoge = new Integer(42);

このコードのままであれば、 同じ数字を宣言したとしてもオブジェクトが生み出され、パフォーマンスが落ちるのではないか? という疑問に対してはYESと答えるしかありません。 例えば、

hoge = 42
huga = 42
hugi = 42

というのはC++での疑似コードでは

auto hoge = new Integer(42);
auto huga = new Integer(42);
auto hugi = new Integer(42);

と表せるはずですから。

ですが、面白いことに実際にはRubyでは工夫されています。 まず、次のようなコードを見てみましょう。

puts 20.object_id
puts 20.object_id
puts 20.object_id

例えばこのコードは、私の環境では以下のように出力されます。

41
41
41

おや?これは少し興味深そうです。冒頭で私は

通常、Rubyではオブジェクトが作成された時、他のオブジェクトと衝突しないようにユニークなオブジェクトIDが割り振られます。

この様に説明しました。とすると、20というオブジェクトは、一回しか作成されておらず、そして同じオブジェクトIDが出ていることから、あたかもそれを使いまわしているかのような挙動をしています。 そのため、このコードのC++での擬似コード

std::cout << (new Integer(20))->object_id();
std::cout << (new Integer(20))->object_id();
std::cout << (new Integer(20))->object_id();

これは真に正解ではなさそうです。

これの種はとてもシンプルです。 Rubyでは全てのIntegerオブジェクトは、シングルトンパターンによってオブジェクト化されます。 つまり、1度登場した数字オブジェクトは、以降同じオブジェクトを使い回すということになります。 次のようなコードで確認してみましょう。

v = 20
puts v.object_id
puts 20.object_id

例えば私の環境では以下の様に出力されます。

41
41

変数 vに代入されている20という数字オブジェクトの object_id と、 そもそもの20という数字の数字オブジェクトの object_id が一致しました。

20というオブジェクトは、一回しか作成されておらず、そして同じオブジェクトIDが出ていることから、あたかもそれを使いまわしているかのような挙動

RubyのIntegerは、シングルトンによって工夫したデザインをする事によって、この挙動を実現しています。 つまり、同じ数字を宣言したとしてもオブジェクトが生み出され、パフォーマンスが落ちるのではないか?に対してはNoと返答する事が出来そうです。 私達は、知らずIntegerの工夫によって、こういった細かいパフォーマンスを気にすること無く、コーディングが出来ているというわけです。

「=」が持つ 本当の意味

さて、ここまで読み進めた方は、一つの違和感を覚えるでしょう。 この記事のタイトルは「=」について紐解いてみるというタイトルでした。 しかし、実際言及したのはRubyという言語が持つ、全てがオブジェクトという性質についてでした。 冒頭で私は、

「=」がもつ働きは、実はRubyという言語の性質を追求するのに実はもってこいという事

などと述べました。ここまでの言及で実は先に、Rubyという言語の性質については追求できましたが、肝心の「=」がもつ働きについては追求できていません。 いやいや、と思うこともあるでしょう。 変数に値を格納 という表現を、同じく冒頭で私はしています。そしてあるいは多くの人が同じような認識を持っているかもしれません。 「変数に値を格納する事」こそが「=」の働きである。これは本当なのでしょうか?

まず、次のようなコードを見てみます。

a = "hoge"
b = a 

puts a
puts b

このコードの出力は自明です。どちらの putshoge を出力することでしょう。 しかし問題はもっと深い所にあります。 この時、 ab が参照している hoge という文字列は果たしてどうなっているのでしょうか? 「格納」という表現が合っているのならば、a に 「hogeを格納」した後、 b に 「a の内容を(コピーして)格納 」といった表現が出来るはずです。 これは直感的には ab にはそれぞれ別々に値が入っているイメージを作ります。 しかし、実際にはどうでしょう。次のようなコードを試してみます。

a = "hoge"
b = a
puts a.object_id
puts b.object_id
puts "hoge".object_id

このコードの出力は、私の環境では以下のようになります。

60
60
80

まず、文字列 hoge 自体に object_id を行った時、違うidを参照していることから、文字列はIntegerとは違い、同じ文字列でも新たな文字列オブジェクトが生成され、別のユニークなobject_idが割り当てられることがわかります。

では、 abはどうでしょうか?こちらは同じobject_idを指しています。 つまり、abが指している文字列オブジェクトは同じものであるという事なのです。 その事実を確かめてみましょう。

a = "hoge"
b = a
a.upcase!
puts a
puts b

upcase! は、格納されている文字列を大文字に変換するメソッドです。そして、このコードの出力は

HOGE
HOGE

となります。 b には何ら作用していないように見えるのに、 bの出力も大文字になってしまいました。 これは、

abが指している文字列オブジェクトは同じものであるという事

という事実を裏付けているものに他なりません。 そして、これは「格納」という表現が期待するものとは違うように思えてきます。 とすると

b = a

というコードが持つ意味は

  • aにbの内容をコピーして格納する

のではなく、

  • bはaと同じオブジェクトIDを持つようにする

という事に他ならないのです。 もっと、スッキリする言い方にするのであれば、

a = "hoge"

というのは、 - aという変数は、生成された文字列オブジェクト "hoge" を参照する

となり、

b = a

というのは - bという変数は、aの参照しているオブジェクトを参照する(=aと同じオブジェクトを参照する)

という言い方が出来る事になります。 つまり、「=」が持つ本当の働きは、右辺のオブジェクトの参照先を、左辺に”束縛”する という事なのです。 「=」は、変数にオブジェクトを格納するものではなく、ただ束縛する、結びつけるという働きを持っているのです。 これがRubyにおける「=」の本当の意味なのです。

インクリメント演算子が無いわけ

「=」の紐解きが完了しました。 今こそ、motivationで生まれた疑問を解消するときです。 まず、自己代入 += は何故許されているのか。 これはとても単純な問題です。

value += 1

これは、次のコードのシンタックスシュガーでした。

value = value + 1

このコードが意味することは、value が参照している数字オブジェクトに対して、「1を足した数字オブジェクト」を新たに生成し、それを新たに左辺の value に束縛している。 という事になり、これはRubyの性質から見ても筋が通っています。

value = 1
puts value.object_id
value += 1
puts 2.object_id
puts value.object_id

このコードは、私の環境では以下のようになります。

3
5
5

1という数字オブジェクトを保持していた valuevalue+1 によって新たに生成された2という数字オブジェクトを value = によって新たに参照するようになった。 この事を考えると、 2.object_id と2回目の value.object_id が同じ値を出力するのは、当然のことと言えます。

さぁ、かたや ++ はどうでしょう。 この ++ は "変数の値を1加算する" という意味でした。 では、次のようなコードを考えてみます

a = 1
b = a
a++

もしこれが許されればどうでしょうか? まず、aに数字オブジェクト1が束縛されます。そして、 b も、 a と同じオブジェクトを参照します。 その後、++ によって、 a が参照している数値に対して1を加算しようとします。この操作は破壊的です。 そうすると、どういった事が起きるでしょうか。

a = 1
b = a
a++
puts a
puts b

このputsは両方とも2を返してしまうわけです。もっと言うなれば、 1.object_id を束縛していたものは、全て「2」になってしまうわけです。1が2になる世界。意味が分からなくなってきました。

数値はプログラミングにとって欠かせません。このような意味が分からないことが起きないように、Rubyでは数値オブジェクトには、Immutableという「破壊的な変更」を出来ないようなルールが課せられています。

そして、このルールを破らないように、Rubyには ++ が無いというわけなのです。 同じ様に、"変数の値を1減算する" デクリメント演算子もありません。 これで疑問が解消できました。

outro

長い旅路でした。 私達は、Rubyにおける「=」の持つ意味、そしてその本質であるところのRubyでは全てがオブジェクトである、というものの片鱗を見ました。 これで明日から「=」を書く時も、実際に行われている事を思い浮かべながらコーディングが出来そうです。

いかがだったでしょうか? 生まれた些細な疑問は、時として実に深くプログラミング言語の性質を追求することになる訳ですから、プログラミングは面白いものです。 日常的に生まれた疑問は、是非とも疑問のまま終わらせず、深く追求していきたいですね。

Baseconnectでは、Rubyの「=」のようにデータを結ぶ(Musubu)事に興味があるエンジニアを募集しています。

herp.careers