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

Ruby 2.5: Enumerableの新機能: トリプルイコール`===`と述語メソッドの合わせ技(翻訳)

概要

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

画像はすべて元記事からの引用です。

Ruby 2.5: Enumerableの新機能: トリプルイコール===と述語メソッドの合わせ技(翻訳)

以下の素晴らしいドキュメントもご覧ください。本記事で書かれていることについて皆さまが気づかなかったことも含めてひととおり載っていて有用です。

トリプルイコール演算子の黒魔術

トリプルイコール演算子に関する以下の私の過去記事を未見の方は、ぜひお読みください。

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

今回の新機能についてここから先のいくつかのパラグラフでやんちゃしています。読んだ方はきっといろいろやんちゃなアイデアで心がいっぱいになることでしょう。

述語メソッド

何はともあれ、私がここで言う「述語(predicate)」が何を指すかおわかりでしょうか?ここでは、Enumerableに含まれる述語スタイルのメソッド、すなわち末尾が?のメソッドを指します。

[1,2,3].all? { |n| n.even? }  # => false
[2,4,6].all? { |n| n.even? }  # => true

[1,2,3].any? { |n| n.even? }  # => true
[1,3,5].any? { |n| n.even? }  # => false

[1,2,3].none? { |n| n.even? } # => false
[1,3,5].none? { |n| n.even? } # => true

これらのメソッドのショートハンドについては後述しますが、ひとまずコード例はこれを元にします。

Enumerablegrep

Enumerableにあるのに忘れられがちなメソッドのひとつに、grepがあります。

[1,2,3, 'string'].grep(Numeric) # => [1, 2, 3]

grepのドキュメントで動作を調べてみましょう。

Pattern === elementとなるすべての要素を列挙する配列を1つ返す。

トリプルイコール演算子にこんな便利な使いみちがあることはとっくにご存知かと思いますが、ひとりの勇敢なRubyistがここに少しばかり手を加える決心を固めました。

トリプルイコールに述語メソッドを食べさせる

いよいよ本題です。

上述の述語系メソッドたちが「今こそ、たったひとつの引数を取って先のgrepのように===に適切に応答する時だ」と決意すると、何が起きると思いますか?

マジックです。マジックが花開くのです。

%w(foo bar baz).none?(/foo/) # => false

[1,2,3].all?(Numeric) # => true

これはこれで面白いですが、マジックとしては小粒です。ではもう少しやってみましょうか。

%w(10.0.0.1 10.0.0.5).all?(IPAddr.new('10.0.0.0/8'))

システム管理者なら泣いて喜ぶところです。しかしこれだけではありません。

楽しい楽しい黒魔術

独自の===を定義する方法はもうご存知ですね?いよいよウサギの穴に飛び込むときが来ました。

ArrayHashに機能を追加するラッパーメソッドをこしらえてみたらもっと面白くなると思いませんか?それはもう指折りの楽しさです。

ここで一言: Rubyではこの書き方はよくありません

前述の「トリプルイコール===の黒魔術」記事からコード例を再録します。

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

このデータで遊ぶには、ラッパーをこしらえておく必要があります。

class Q
  def initialize(conds = {}) @conds = conds end

  def ===(other)
    @conds.all? { |k, matcher| matcher === other[k] }
  end
end

def Q(conds = {}) Q.new(conds) end

それではやってみましょう。

people.any?(Q(age: 1..10)) # => true

できました!もっとも技術的にはto_procでも同じことはできますが。

class Q
  def initialize(conds = {}) @conds = conds end

  def ===(other)
    @conds.all? { |k, matcher| matcher === other[k] }
  end

  def to_proc
    -> other { self === other }
  end
end

def Q(conds = {}) Q.new(conds) end

people.select(&Q(age: 20))

これも面白いですね。少なくとも私は面白いと思いました。

もういくつ寝ると使えるの?

いつになるかはまだはっきりしませんが、Matzがこれに祝福を与えました。

了解、ご提案どおりオプション1に決めました。
今後は以下のように書けます。

[1, 3.14, 2ri].all?(Numeric) # => true

if should_be_all_symbols.any?(String)
  ...
end
some_strings.none?(/aeiou/i)

Matz.
11286#note-16より

断言はできませんが、運がよければ今度のクリスマスの2.5でお目にかかれると思います(訳注: ご存知のとおりRuby 2.5で導入済みです)。いずれにしろ待ち遠しくてなりません。

キヅネザルはお好き?

ところで先ほどから本記事でキツネザルくんがちらちら姿を見せていることにお気づきでしょうか?この子たちは今私が執筆中の本からやってきたお客さんです。

baweaver.gitbooks.ioより

最後に

皆さん同様、私も新機能というものが大好きです。Rubyのバグトラッカーを追って書いた記事につき、読みにくさについてはご容赦ください。コアチームが素晴らしい仕事をしてくれたおかげで、私たちは一足早くクリスマス気分を迎えました。

関連記事

Ruby: ループには一時変数ではなくEnumerableを使おう(翻訳)

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


CONTACT

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