Rubyのtrueとfalseの話

  • 60
    Like
  • 4
    Comment

 この記事は、技術系同人誌としてまとめるはずだった原稿をほぼそのまま転載しています。諸事情により向こうかなり長い間同人誌即売会に売り手として参加することが難しくなったためです。
 長いですが、お楽しみいただければ幸いです。

まえがき

 この本は、Rubyコミッタである卜部昌平に、その妻である私、卜部一恵がRubyのtrueとfalseについて突っ込んで聞いてみた話です。本文は両者の対話形式で進んでいきます。
 私は昌平と同じ大学同じ研究室に所属していたのでプログラミングについての基礎は一応ありますが、エンジニアとして職を得たことはありません。つまり、プログラミング初級者です。この本はそのくらいのレベル感の本だと思います。
 私自身が初級者なりにRubyを使っていて、if文が思った通りに動かない、そんなときに抱いた疑問からこの本が生まれました。
 同じような疑問を抱いている方の一助になれば幸いです。

はじめに

K:この本は卜部昌平と卜部一恵の対話形式で構成されています。Sは昌平、Kは一恵です。よろしくお願いします。

S:よろしくお願いします。

K:今回はRubyのtrueとfalseの話ということで、私がRubyを使っていて気になったところを訊いていこうと思います。結構しつこく訊くかも。

S:頑張って答えます。

K:では、張り切って疑問その1から行きたいと思います。

trueクラスとfalseクラス、そしてBooleanの話

Boolean型は存在しない

K:まず、trueがtrueに、falseがfalseになる理由について訊きたいと思います。
この二つはboolean型の定数ですよね?

S:いえ、違います。そもそも型ってなんでしょう?
K:例えば、

int n = 20        # => 型

new Int m = 30    # => クラス


みたいな感じに思っていたのだけれど、違うのですか?

S:その違いがあるのは、Javaですね。でも、Rubyは両者に実は差がないんです。

K:え!?なんでですか?

S:例えば、値というものに型なりクラスなりが付いている。それとは別に、変数にも型なりクラスなりがある。この二つは区別して考えなければなりません。

そう考えると、まず値に関しては、Rubyでは全部クラスであって、型ではない。ここで言う『型ではない』という言葉は、int n = 20int に相当するものがない、という意味です。後で説明するけど、Rubyという言語の仕様における型が、値には存在します。

次に変数に関して言うと、Rubyは変数には型が付いていない。クラスも付いていない。『動的変数』という言葉の意味がこれです。
なので、じつは int とか new Int とか書かなくてもいい。というか、書いちゃいけない。

K:サンプルコードを作ってみます。


int n = 20
p n

これ、確かに実行できないですね。 undefined method ‘int' for main:Object (NoMethodError) となります。

こう書けないのは動的変数であることを明示的にするためですか?

S:わざわざ複雑に書かなくていいじゃないですか。むしろなんで書きたいの?って感じ。

K:例えば、この変数には数値だけが入っているということを明示したいとかです。明示すると、数値が入っているはずの変数に文字列を入れてしまって結果バグるという未来を回避できると思います。

S:そうですね。それは一面で正しいけれども、Rubyの場合は今はそうなっていない。未来的にはこの変数には数値だけが入っているということを保証できるようになるかもしれないけれど、それでもプログラマが明示的に型を書くという形にはならないと思います。

K:それはなぜですか?

S:JavaScriptやSwiftでもそうだけれど、今は型を書かないのが流行しています。それはなぜかというと、 n=20 と書いてあれば、nが整数なのは明らか。後からそれに文字列を入れようとするとエラーになって欲しいんだけど、今はその機能を実現するためにわざわざ型を書かなくても良くなったんです。なぜなら、代入が最初にあった時点で、右辺を見て勝手に判断できるようになったからですね。

実はRubyにはまだこの機能はないのだけれど、今後そうなっていくのだと思います。

K:なるほど。では、今のところRubyは動的変数を用いていて、型がない言語である、という訳ですね。Rubyは型がない、とはよく聞いていましたが、ようやくその意味がわかった気がします。

S:でも注意して欲しいのは、実際には『変数に』型がないだけであって、『値には』型がある。例えば20っていうのは数値だけれど、これを文字列として扱います、ということにはならない。

K:print(20) は動かなくて、 print(20.string) にしないとダメだということですか?

S:いや、 print(20) は実は変換されちゃうな。


print(20 + "30")
print("20" + "30")

にすると、上は動かなくて下は動く、という話です。

K:納得しました。

ちなみに、 print(20) が普通に20と表示されるのはなんでなんでしょう?

S:printはなんでも引き取って文字列っぽく表示するすごい関数で、数値を引き取っても勝手に文字列にしてくれるからですね。本当に文字列しか表示しない関数は、実はない。

K:ふむふむ。ちゃんと言語の仕様と関数の仕様を分けて考えないと混乱しますね。

さて、ここでboolean型がないことに話を戻して考えてみると、押さえておくべきことは、値には型がある、変数には型がない、ということですね。

とすると、trueという値とfalseという値はあるけれど、変数にbooleanという属性のようなものはつけられない、ということで、boolean型がない、ということの意味になりますか?

S:そういうことになります。

Booleanクラスも存在しない

K:ではここで、trueという値とfalseという値、これらとtrueクラスとfalseクラスについて訊いていきたいと思います。

まず、trueクラスがあり、falseクラスがあり、そうするとBooleanクラスに入れるものがないので、Booleanクラスは存在しない、という話を聞いたのですが、とするとBooleanは型もクラスも存在しないということなんでしょうか?

S:はい。

K:例えば、Booleanというクラスがあってtrueクラスとfalseクラスはそれを継承している、という話でもない?

S:でもないです。その理由はあって、継承というのは親があって子がある、親クラスの振る舞いと子クラスの振る舞いは共通しているから子クラスで親クラスの挙動をちょっとだけ変えて使う、というのが継承の基礎になります。

けれども、trueとfalseは正反対の性質なので、同じ挙動ではない。なので、trueとfalseの共通の親は用意していないんです。

K:そうなんですね。なんかコンパイルすると真偽値になるクラスがあったとして、そのクラスのパラメータを一つ変えるだけでtrueとしての振る舞いと、falseとしての振る舞いを切り替えられるのかなぁ?と漠然と思ってましたが、全くそんなことはないのですね。

S:全くそんなことはないです。

K:とすると、trueクラスとfalseクラスさえ用意すれば、Booleanクラスはいらない、という話が納得できてきました。

trueクラスとfalseクラスについて

K:ではここまでの話を踏まえて、trueクラスとtrueという型の値、falseクラスとfalseという型の値の関係性について訊いていきたいと思います。

まず、trueという型の値は、trueクラスのインスタンス、という理解でいいでしょうか?

S:いいでしょうか?の答えは、ハイです。true型の値、ということと、trueクラスのインスタンス、ということは同じことを別の言葉で言っているだけのこと。

K:またちょっと脱線しますが、trueのインスタンスは一つのプログラムで複数存在することはありますか?

S:ないです。

K:とすると、true.object_idは全て同一なんですね。これはfalseに関してもそうですか?

S:そうですね。

K:そうすると、true == truefalse == falseは常にtrueになるんですね。ここは当たり前だけど、ものすごく大切なところですね。

そして、trueはtrue、falseはfalseだと、定義・実装しているから、そう動くと。

ここでこれらの振る舞いについて注意しておいたほうがいいことはありますか?例えば、変数にfalseを代入したはずなのにtrueと判断されてしまう、とか。

S:それはあまりない気がしますね。

ゼロを真にした話

K:ゼロって、例えばCなどではfalseと判断され、そのことを前提としたアルゴリズムもあったりすると思うのですが、Rubyではここがtrueなのは何故なのでしょうか?

S:そもそもCには、歴史上最初にはfalseが無かったのです。今はありますが。なので無いなら何か他のものを代わりに使わなければならない、そのためにゼロが使われていたんです。

K:はー、そんな歴史があったのですね。

S:でも、Rubyには最初からfalseがあるので、無理してゼロのことをfalseだと思わなくていいのです。

もうちょっと一般的な話をすると、世の中のプログラミング言語というのはだいたいifの条件文の中に何が書けるのかということが何パターンかあります。ひとつは真の値trueと偽の値falseしか書けず、他の値は何らかの方法でtrueかfalseに変換するという言語。もうひとつは偽になる値、例えばゼロ、以外は全部真になるという言語。だいたいこの二つのどちらかです。

K:Rubyは後者だと考えていいですか?

S:だいたいそうなんだけど、Rubyにはnilがあって特殊だね、とは思います。

nilクラスについての話

K:では、nilクラスについて聞きたいと思います。まず、 nil == true はfalseですよね?

S:はい。

K:nilをfalseにするのは、言語仕様としてそう決めているわけですよね?

S:混乱してきましたね。用語の整理をしたいです。

まず、falseというのはfalseクラスのインスタンス、ということにしましょう。次に、if文の中に書いたときにelseの方に行くという現象、これはfalseとは別の名前で呼びたいので、日本語の『偽』という言葉を使いましょう。

そうすると、nilは偽です。

K:nil == false はtrueでは無い、ということですか?

S:はい。試してみましょう。

irbで次のように打ってください。


nil == false

これは実行されると、 => false になるはずです。

K:おお、本当だ!では、nilとは何なのでしょう?

S:nilというのは、例えば変数がまだ初期化されていないなど後から何かが入ってくるけどとりあえず場所だけある、つまり、『何も無いという印』のことです。

なので、本当はtrueとfalseとnilで三分岐するif文を考えることもできたかもしれません。でもそれはややこしいので今は普通に二分岐するif文ができていて、thenの側とelseの側と、どちらにnilを分岐させるか考えたときにelseの側に行ってくれたほうが便利、という理由で『偽』になっています。

K:なるほど。if文の中に書いたときに『偽』にはなるけれど、falseとは別の役割があるから、 nil == false はfalseになるのですね。

そうすると、nilは一つのプログラムの中で複数インスタンスがあるのでしょうか?

S:それについてはtrueとfalseと同じように、インスタンスは一つです。

K:配列の中などに複数のnilを入れることもできますが、それでもインスタンスは一つなのでしょうか?先程聞いたように何も無い場所を指し示しているのであれば、インスタンスは複数あっていい気がします。

S:それでも一つです。

例えば、 x=“hoge” という式があります。これは、 x という変数の箱の中に ”hoge" というオブジェクトが入っているのかというと、Rubyの場合実は違うわけです。nという変数の箱の中には ”hoge" というオブジェクトを指し示すメモリアドレスが入っています。

K:Rubyは全部ポインタ!という話ですね。

S:そうです。ここで、yという変数だけ用意したいと考えたとき、それが難しい。なので、 y=nil と書いて y という変数があり、それが nil というオブジェクトを指し示していることを、 y という変数だけ用意した、という意味として考えよう、という工夫をしたのです。

なので、 y=nil でも z=nil でも、右辺の nil は同じオブジェクトが指し示されているだけなのです。

K:だからnilもインスタンスは一つだけなんですね。

そして、nilは『偽』として振る舞ったほうが便利だからif文に入れたときに『偽』になるけれども、その本来の役割は空き地を表すことであって、falseであるということとはまた違う、という理解でいいでしょうか?

S:そうです。

配列やハッシュがnilだったり空だったりするときの話

K:では、配列やハッシュがnilを指し示していたときの話です。これらがtrueとなるのは以下のコードで確認できます。

a = [nil]
if a then
    p "true"
end

これは ”true" を出力します。

しかし一方で、


a = [nil]
a == true

は、 false となります。

この振る舞いの差はどうして生まれるのでしょう?

S:ここでもやはり用語を整理しましょう。trueクラスのインスタンスのことをtrueと呼びましょう。

一方、if文の中に入れたときにthenの側に来ることを、日本語で『真』と呼ぶこととしましょう。

K:先ほどの例に習うと、 a = [nil] は『真』ではあるけれどもtrueでは無い、ということで、後者の比較式はfalseになるのですね。

S:そうですね。
nilが『偽』になったときと同じような議論です。

K:では、先程nilは『偽』として扱うという話になったのに、配列から指し示した途端に『真』の側に行ってしまうようになったのは何故なのでしょうか?

S:それは、配列が『真』だからです。

真偽を判定するときに、配列の中まで見に行って配列の中身で真偽が変わるということはありません。

K:それはハッシュでも同じなんですね。ただの変数のときだけは、中身を見に行ってnilを『偽』だと判定すると。

S:そうです。変数と配列が同じようにポインタを使っているというのは鋭い着眼点ですね。

K:さらに、配列の中身を見に行かないということは、空配列であっても、『真』と判断してしまうということでしょうか?

S:はい。

K:けれども配列やハッシュが空であるときを真偽値に絡めて判定したいという需要は確実にあります。それを行う関数として、 .empty? があるのですね。
この .empty? の挙動についてまとめたいです。

まず、次のコードで実験です。


a = []
a.empty?

これは true になります。

一方、

b = [nil]
b.empty?

これは false です。

配列aが空配列であるということと、配列bの先頭が空き地を表すという意味を持っていること、この二つが .empty? という関数で違う値を返す理由は何故ですか?

S:これは難しいですね。難しいけど、配列の場合はemptyというのは中身がどうという話ではなく、サイズがゼロかどうかというふうに決めました。それだけが理由です。

でも、中身まで見てnilしか無いかどうかを確認したい、という需要も確かにあります。そのための関数はすでにあって、 .compact というものがあります。この関数を配列に対して実行すると、nil以外のものを残します。

この .compact を使った結果、空配列になったのならば、その配列の中にはnilしかなかったということになります。

K:つまり、


b = [nil]
b.compact.empty?

とやれば、 true が返ってくるということですね。
うまく使い分けたいところです。

ところで、 .compact はハッシュに対しては定義されていないようですがこの場合はどうすればいいでしょうか?

S:.delete_if{} がありますので、それを使えば似たようなことできます。けれども、あまり一般的では無いかもしれません。ハッシュに対してはニーズがあまり無いのかも。

配列やハッシュにtrueやfalseを入れたときの話

K:では、配列やハッシュにtrueやfalseを入れた場合ですが


a = [false]
if a then
    p "true"
else
    p "false"
end

これは ”true” を返します。これも先程と同じ結論でしょうか?つまり、配列やハッシュにtrueやfalseを入れても、中身まで見にはいかないから『真』だと判断されると。

S:その通りです。

K:ちなみに、以下のコードだとちゃんと配列の中を覗きに行くから配列に入れた真偽値が反映されるんですね。

a = [false]
if a[0] then
    p "true"
else
    p "false"
end    # => “false”

S:そうですね。

K:多少脱線しますが、ハッシュのキーにtrueやfalseを入れるのは大丈夫なんでしょうか?

S:やってみればわかりますよ。

K:やってみます。


a = {true => "hello", false => "bye"}
p a[true]                  # => "hello"

p a.key("hello")           # => true

if a.key("hello") then
    p "true"
end                        # => "true"


大丈夫ですね。trueやfalseは予約語なので使えないかと思ってました。

S:実際には使えますね。

自作クラスの真偽の話

K:あとは自作クラスについて聞きたいのですが、例えば、次のようなコードはどうでしょう?

class BoolTest
end

test = BoolTest.new

if test then
    p "true"
else
    p "false"
end

これは ”true" が出てきます。

S:そうですね。

K:これもまた配列の時と同様に、クラスのガワだけを見て中身は気にしない、そしてクラスは『真』だと定義されているから、という理由で良いのでしょうか?

S:はい、その理解ですごく正しいと思います。

K:例えばコンストラクタを作っていないなどの理由でnewだけしてもクラスの中身が何も無いという場合も、そのままifの条件文の中に入れたらtrueになりますね。

ここで、classの中身に何かがあるか、何もないことを判定するにはどうしたら良いのでしょうか?

S:結論だけ言うと、そういうことは気にしないように作るのがRubyのやり方です。

K:とすると、適当にクラスを作っても .empty? みたいなメソッドは無いということになるのでしょうか?

S:そもそもそのメソッドが必要とされるシーンはどういったシーンですか?

K:例えば、車を買おうとするときに欲しい車がまだ見つかっていないなら wanted_car = Car.new として、見つかったら wanted_car = prius などとするようにしておいて、wanted_carが見つかっているか見つかっていないか調べたい場合です。ここでpriusはCarクラスのインスタンスだと思ってください。

S:そういうことに使うならクラス自体に .empty? が必要そうですね。

けれども、そのようなメソッドはクラスに対しては用意されていません。なので、それが必要なら自分で実装するようにしてください。

結論:条件文には真偽値だけ書けるようにしておけばよかったんだよ!

K:ここまでさまざまなtrueとfalseの話をしてきました。RubyにはそもそもBooleanというクラスも型も無い、という衝撃的な話から、trueであることと『真』であることが違う、falseと『偽』についても同様という話など、大変参考になりました。

でも、これらは結構ややこしいなぁ、というのが正直な感想です。昌平さんはどうですか?

S:つまりですね、ifの条件文には真偽値だけ書けるようにしておけばよかったんですね。

K:なるほど。配列やクラスなどが直接ifの条件文の中に置けるからややこしくなる、という話ですね。

でも、もしもこれを今から是正するとなると、Rubyで書かれた既存のコードとの互換性を確保するのが相当難しいですね。

S:既存のコードとの互換性はなくなりますね。このifの条件文に置けるものを変えると、既存のコードは一切動かなくなる。
だから、このややこしさを解消するためだけにifの条件文に置けるものを変える、ということは無いでしょう。

K:それもそうですね。

このややこしさを覚えておくために抑えるべきところは、以下の点で良いでしょうか?

  1. trueクラスのインスタンスがtrueという値
  2. falseクラスのインスタンスがfalseという値
  3. trueという値でもfalseという値でも無いが、ほとんどのオブジェクトはその中身に関係なく『真』だと判断される。
  4. ただし、nilオブジェクト、及び変数の中身のnilは『偽』と判断される。

S:はい、いいと思います。足りないところも無いです。

K:ありがとうございます。

あとがき

 お楽しみいただけたでしょうか?
 この本を作るにあたって昌平にいろいろ質問をしたのですが、こちらがどう思っているのかを的確に察しながら答えを返してくれたことに感謝しています。
 この本を作るためにいろいろ質問して理解したことのうち、『真』であることとtrueが違う、『偽』とfalseについても同様、という話が個人的に一番衝撃的だったかもしれません。『真』であることはtrue、『偽』であることはfalseと頭っから覚え込んでいたので、ここが違うとわかった時、頭の靄が晴れるような思いをしました。この本で質問した多くのことは、値がtrueであることと『真』の分岐に向かうこと、値がfalseであることと『偽』の分岐に向かうこと、この両者をごちゃ混ぜに考えているから生まれたもののように思います。
 ここがはっきりしたのが私個人にとっては大きな収穫ですし、読者の方も同じような収穫をこの本の中から得てくれていたとしたら、それに勝る喜びはありません。

謝辞

 この本のレビューをしてくださいました、@tatsuoSakurai氏に感謝します。

86contribution

『真』であることとtrueが違う、『偽』とfalseについても同様

truthy とか falsy って呼び方をしたりもしますね

214contribution

@ttanimichi さん
なるほど。この件、英語圏ではどう表現してるんだろう?と疑問に思っていたので、納得です。
ありがとうございます。

2contribution

「配列やハッシュにtrueやfalseを入れたときの話」のハッシュのキーにtrue/falseを使うコードで
trueがtureと書いてありますよ

214contribution

@stmkza さん
ご指摘ありがとうございます。
他の方からもご指摘があり、訂正しました。