Tech Racho エンジニアの「?」を「!」に。
  • 開発

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: 紛らわしい条件文を書かないこと(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。