Ruby inside: トリプルイコール === の黒魔術(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby inside: トリプルイコール===の黒魔術(翻訳)

トリプルイコール演算子===は、ほとんどの場合無視されるかcase文の背後でひっそりと使われているだけにとどまっています。===について深掘りしている記事はいろいろありますが、これには一体どんな使いみちがあるのでしょうか?

トリプルイコール演算子===とは何か

デフォルトでは、ダブルイコール演算子==の単なるエイリアスでしかありません。トリプルイコールは、他のクラスが独自の振る舞いのためにオーバーライドしている部分で輝きを放ちます。いくつか例をご紹介しましょう。

範囲の比較

(1..10) === 1

範囲におけるトリプルイコールは、includes?のエイリアスです。

なお、範囲はそれ自身が黒魔術のかたまりです。これだけで別記事になるトピックなので今後のお楽しみにするとして、ここではほんの一端だけをご紹介します。

'a'..'z'
'1.0.0'..'2.0.0'

正規表現の比較

/abc/ === 'abcdef'

正規表現におけるトリプルイコールは、matchです。

クラスの比較

String === 'foo'

クラスにおけるトリプルイコールは、is_a?です。

procの比較

-> a { a > 5 } === 6

procの場合、callを呼びます。この後で面白い部分をご紹介します。

IPAddrの比較

IPAddr.new('10.0.0.0/8') === '10.0.0.1'

はい、ご覧のとおりの動作です。IPAddrはトリプルイコールをオーバーライドして、比較対象がサブネットに含まれているかどうかをチェックし、渡したものが何であっても比較できるようにご丁寧にもIPAddrに強制変換までしてくれるのです。この機能を特に愛するのはシステム管理者でしょう。

一応caseの比較も

訳注: just in caseは「念のため/一応」と「caseの場合」のダジャレです。

トリプルイコールの振る舞いをいくつか見てきましたが、Rubyでの主要な使いみちと言えばやはりcase文でしょう。

case '10.0.0.1'
when IPAddr.new('10.0.0.0/8') then 'wired connection'
when IPAddr.new('192.168.1.0/8') then 'wireless connection'
else 'unknown connection'
end

しかしこれでは面白くも何ともありません。もうちょっとぐらい魔法っぽいことしたいですよね?ある理由に基づき、私たちには「ダックタイピング」という強い味方があるのですから、これを繰り出さない手はありません。

クエリ

これを見かけるのは多くの場合ActiveRecordででしょう。たとえばPersonモデルにクエリをかけるには次のようにします。

Person.where(name: 'Bob', age: 10..20)

普通の配列にもこんな感じでクエリをかけられたらいいと思いませんか?範囲や正規表現などでのトリプルイコールの振る舞いを思い出しましょう。さあここからがお楽しみです。

まずはハッシュの配列で実験を開始しましょう。

people = [
  {name: 'Bob', age: 20},
  {name: 'Sue', age: 30},
  {name: 'Jack', age: 10},
  {name: 'Jill', age: 4},
  {name: 'Jane', age: 5}
]

さて、ここで20歳以上の人を全員取得したいとしましょう。ありきたりなRubyコードならこんな感じになるでしょう。

people.select { |person| person[:age] >= 20 }

でもこれで条件がさらに増えたらごちゃごちゃしそうですよね?大量のJSONを食わせようとしたときにこれでは相当つらくなるでしょう。そこでActiveRecordの書き方をちょいと拝借します。

def where(list, conditions = {})
  return list if conditions.empty?

  list.select { |item|
    conditions.all? { |key, matcher| matcher === item[key] }
  }
end

トリプルイコール演算子がしれっと使われていることはもうおわかりですよね。このおかげで、完全一致のみならず、上述のさまざまな一致をどれでも使えるようになるのです。

JSONパケットダンプ

JSONで何らかのパケットダンプ、たとえばこんなふうにクエリをかけるとしましょう。

where(packets,
  source_ip: IPAddr.new('10.0.0.0/8'),
  dest_ip:   IPAddr.new('192.168.0.0/16'),
  ttl:       -> v { v > 30 }
)

ここでご注意いただきたいのは、JSONはパース中に特に指定のない限りStringのキーを渡す点です。慎重にやりましょう。

どうも魔力が今ひとつですね。私たちにはProcという強い味方にもアクセスできることを思い出しましょう。これがあれば、コンポジションをフルに用いてRamda gem↓のような実に面白い複合クエリをRubyで実現できるのです。

ではRamdaでもっとカッコよく書いてみましょう。

where(packets,
  source_ip: IPAddr.new('10.0.0.0/8'),
  dest_ip:   IPAddr.new('192.168.0.0/16'),
  ttl:       R.gt(30)
)

さらに魔力をアップさせることも可能ですが、それはまた別の記事で。今は、RubyがProcに与えた===によって、Ramdaのようなカリー化フレームワークを用いて本物の黒魔術を使えるということだけ申し上げておきましょう。

オブジェクトの比較

よろしい、それでは今度はいくつかのオブジェクトがあるとしましょう。十分シンプルですね。先ほどのitem[key]を単にitem.public_send(key)に置き換えると次のようになります。

def where(list, conditions = {})
  return list if conditions.empty?

  list.select { |item|
    conditions.all? { |key, matcher|
      matcher === item.public_send(key)
    }
  }
end

特にこの部分の魔力が強いと感じるのであれば、Rubyのハッシュキーにはどんなオブジェクトでも使えるという事実につけこんでさらに凶悪な魔法を繰り出せます。

def where(list, conditions = {})
  return list if conditions.empty?

  list.select { |item|
    conditions.all? { |key, matcher|
      matcher === item.public_send(*key)
    }
  }
end

キーを爆破することで、こんなこともできるようになります。

where([[1,2,3], [20,30,40]],
  [:reduce, 0, :+] => R.gt(20)
)

私には実用的な使いみちがちょっと思いつきませんが、間違いなく楽しめます。

最後に

トリプルイコール演算子の裏技はまだまだあります。本記事で紹介したのは文字どおり氷山の一角でしかありません。関数の合成(functional composition)という技を合わせれば、数えきれないほどの莫大なマジックを発掘できることでしょう。

おそらくマジックは皆さんにとって関係ないものだと思いますが、それでもcase文で少々てこずったときには役に立ちます。

関連記事

Rubyの===演算子についてまとめてみた

Ruby: 私たち、もしかしてat_exit?を使いすぎ?(翻訳)

Ruby: 紛らわしい条件文を書かないこと(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ