1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Enumerable#grep などでも when 節のように複数のパターンを指定したい

Last updated at Posted at 2022-07-20

case 式で when 10..20, -20..-10 とはできるのに、 Enumerable#grepary.grep(10..20, -20..-10) とはできない。これをどうにかしてみたいお話(小ネタ)。

Ruby バージョン: 3.1.2

背景

Ruby では case 式の条件分岐に使われる === という演算子がある。通常は == と同じだが、いくつかのクラスでは条件分岐が便利になるよう再定義されている。

def judge(obj)
	case obj
	when true, false
		puts "%p is a boolean object." % obj
	when 10..20, -20..-10
		puts "abs(%p) is between 10 and 20." % obj
	when /e/
		puts "%p contains a character 'e'." % obj
	when Exception
		puts "%p is an instance of Exception." % obj
	when :frozen?.to_proc
		puts "%p is frozen." % obj
	else
		puts "%p did not match the defined patterns." % obj
	end
end

[
	false,
	-12,
	:Antares,
	(Math.log(-1) rescue $!),
	-"Antarctica",
	self,
].each { |obj| judge(obj) }
output
false is a boolean object.
abs(-12) is between 10 and 20.
:Antares contains a character 'e'.
#<Math::DomainError: Numerical argument is out of domain - log> is an instance of Exception.
"Antarctica" is frozen.
main did not match the defined patterns.

また、 Enumerable#grepEnumerable#all? などでも同じく === で判定していて、ブロックを書かず楽に条件を指定できるようになっている。

ary = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

# パターンを指定する方法
ary.grep(10..20)                    # Range を指定
ary.grep((10..20).method(:cover?))  # Method を指定
#=> [11, 13, 17, 19]

# ブロックを渡す方法 ※ grep に相当するメソッドは select
ary.select { |x| (10..20).cover?(x) }
ary.select(&(10..20).method(:cover?))
#=> [11, 13, 17, 19]

しかし、これらのメソッドで渡せるパターンは1つのみ。 when 節のように複数は書けない。

(-100..100).step(7).grep(10..20, -20..-10)
# wrong number of arguments (given 2, expected 1) (ArgumentError)

特段困った経験は無いが、 === で判定するという共通点からすれば複数書けてもいい気がする。

解決案

引数にパターンをひとつしか渡せないのなら、「内部で複数のパターンを評価するオブジェクト」を作って渡してしまえばいい。具体的には、例えば以下のように使えるクラス Patterns を定義する。

(-100..100).step(7).grep(Patterns.new(10..20, -20..-10))
#=> [-16, 12, 19]

このクラスは2つの要件を満たせばいい。

  • コンストラクタで任意の数のパターンを受け取って記憶する
  • インスタンスメソッド #=== を再定義し、それらのパターンを順次評価する
    • 各パターンの評価には再び === を用いる
    • 結果を集計して真偽値を返す

when 節だと「複数パターンのいずれか」と評価するので、同様の評価をさせるには Enumerable#any? を使うと便利。(今回は欲張りに #any? 以外も選べるようにしてみる)

class Patterns
	def initialize(*patterns, cond: :any?)
		@patterns = patterns.method(cond)
	end

	def ===(obj)
		@patterns.call { |pattern| pattern === obj }
	end
end

副次効果

cond というオプションで評価方法を選べるようにしたことで、別の使い道が生まれる。

複数パターンの評価方法を #all? に変えれば、今度は case 式で「すべての条件を満たす場合」という書き方もできるようになる。

multiple_of_3 = -> x { x % 3 == 0 }
multiple_of_5 = -> x { x % 5 == 0 }

(1..100).each do |x|
	puts case x
	when Patterns.new(multiple_of_3, multiple_of_5, cond: :all?)
		"FizzBuzz"
	when multiple_of_3
		"Fizz"
	when multiple_of_5
		"Buzz"
	else
		x
	end
end

また #none? を使えば否定も作れるし、入れ子にして複雑な条件も表せる。実行速度はともかくとして、色々な使い方ができるかもしれない。

付録

#=== を再定義しているクラス

デフォルトでは同値性判定になっている。

メソッド Object#== の別名です。 case 式で使用されます。このメソッドは case 式での振る舞いを考慮して、各クラスの性質に合わせて再定義すべきです。

一般的に所属性のチェックを実現するため適宜再定義されます。


所属性判定などの形に再定義しているのは、組み込みや標準添付ライブラリをざっと探すと以下があった。他のメソッドのエイリアスに過ぎないものと、固有の判定になっているものがあるため、詳細は各ドキュメントを参照。

クラス #=== の判定基準 類似の方法
Gem::Requirement バージョン指定を満たすか req.satisfied_by?(obj)
IPAddr IPアドレス範囲に含まれるか ip.include?(obj)
Method (あるオブジェクトをレシーバーとする)メソッドに引数を渡して評価 method.call(obj)
Module そのクラス(継承・ mix-in を含む)のインスタンスか obj.kind_of?(mdl)
Proc プロシージャに引数を渡して評価 proc.call(obj)
Range オブジェクトが区間に属するか range.cover?(obj)
Regexp 文字列が正規表現にマッチするか regexp.match?(obj)
Set オブジェクトが集合に属するか set.member?(obj)

#=== を利用するメソッド

主なものは Enumerable にある。このモジュールを mix-in しているリスト的なクラスで利用できる。

  • #grep, #grep_v : 要素の抽出
    • パターン指定が必須
    • ブロックを追加した場合、要素を変換して返す( #map と同様)
  • #any?, #all?, #none?, #one? : 統計的判定
    • パターン、ブロック、何もなし(要素のまま評価)を選べる
  • #slice_after, #slice_before : リストの分割
    • パターンとブロックを選べる
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?