概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Triple Equals Black Magic – Ruby Inside – Medium
- 原文公開日: 2017/10/16
- 著者: Brandon Weaver
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
文で少々てこずったときには役に立ちます。