[ruby-core:87718] [Ruby trunk Feature#14869] Proposal to add Hash#===

From: manga.osyo@...
Date: 2018-06-30 10:47:49 UTC
List: ruby-core #87718
Issue #14869 has been updated by osyo (manga osyo).


ご意見ありがとうございます。

> ほとんどの用途は Hash#<= で足りているようにみえます。

機能としては `Hash#<=` と類似していますが、『`Hash#===` を定義する事で case-when などで使用することが出来る』というのが主な提案理由となっております。

> {} <= user は true になるので、{} === user も true の方が <= の値の比較を === で行うだけのものということで、わかりやすいのではないかと思いました。

確かに『`Hash#===` は `Hash#<=` の `#===` で比較する版』みたいな説明だと理解しやすそうですね。
ただ、 `{} === user` を `true` にしてしまうと次のような case-when で互換性が壊れてしまうので、互換性を考えて `false` を返すのが妥当かと思います。
互換性を壊してまで `#<=` の挙動に合わせる必要性はなかな、と。

```ruby
def check n
    case n
    when {}
        "空だよ〜"
    else
        "空じゃないよ〜"
    end
end

p check({})   # => "空だよ〜"

# {} === user を true にしてしまうと結果が変わってしまう…
p check({ name: "mado" })   # => "空じゃないよ〜" と期待する
```


----------------------------------------
Feature #14869: Proposal to add Hash#===
https://bugs.ruby-lang.org/issues/14869#change-72737

* Author: osyo (manga osyo)
* Status: Open
* Priority: Normal
* Assignee: 
* Target version: 
----------------------------------------
## 概要

`Hash#===` を追加する提案になります。


## 仕様

レシーバのキーの要素と引数のハッシュのキーの要素を `#===` で比較して、全てが真なら `true` を返し、そうでないなら `false` を返す。
また、レシーバが空のハッシュの場合、引数が空のハッシュなら `true` を返し、そうでないなら `false` を返す。

```ruby
user = { id: 1, name: "homu", age: 14 }

# name 要素が data にあるので true
p ({ name: "homu" } === user)
# => true

# 複数の要素があっても OK
p ({ id: 1, name: "homu", age: 14 } === user)
# => true

# name 要素が user にあるが、値が違うので false
p ({ name: "name" } === user)
# => false

# キーの要素が引数にないので false
p ({ number: 42 } === user)
# => false

# 1つでもキーの要素がない場合も false
p ({ id: 1, name: "homu", number: 42 } === user)
# => false

# レシーバが空のハッシュなら false
p ({} == user)
# => false

# 空のハッシュ同士なら true
p ({} == {})
# => true

# 引数がハッシュ以外なら false
p ({ id: 42 } == 42)
# => false

# #=== で比較しているのでこういうこともできる
p ({ name: /^h/ } === user)
# => true

p ({ age: (1..20) } === user)
# => true

p ({ age: Integer } === user)
# => true
```


## ユースケース

### バリデーション

case-when では `===` を使用して値を比較しているので、`Hash#===` を利用することで次のように条件分岐を行うことが出来る。

```ruby
def validation user
	case user
	# name に対するバリデーション
	when { name: /^[a-z]/ }
		raise "名前の先頭が小文字の場合は登録できません"
	# age に対するバリデーション
	when { age: (0..20) }
		raise "0〜20歳は登録できません"
	# 各要素が任意のクラスのインスタンスかどうかのバリデーション
	when { id: Integer, name: String, age: Integer }
		true
	else
		false
	end
end

# 条件を満たしているので OK
mami = { id: 1, name: "Mami", age: 21 }
validation mami
# => true

# name が小文字から始まっているので NG
mado = { id: 2, name: "mado", age: 13 }
validation mado
# => 名前の先頭が小文字の場合は登録できません (RuntimeError)

# age が 0〜20歳以内なので NG
homu = { id: 3, name: "Homu", age: 14 }
validation homu
# => 0〜20歳は登録できません (RuntimeError)
```

### `Enumerable#grep`

`Enumerable#grep` は内部で `===` を使用した比較を行っているので、次のように任意の Hash のキーの要素に対して検索を行うことが出来る。

```ruby
data = [
	{ id: 1, name: "Homu", age: 13 },
	{ id: 2, name: "mami", age: 14 },
	{ id: 3, name: "Mado", age: 21 },
	{ id: 4, name: "saya", age: 14 },
]

# 特定の要素が含まれている Hash のみを絞り込む
p data.grep(name: /m/)
# => [{:id=>1, :name=>"Homu", :age=>13}, {:id=>2, :name=>"mami", :age=>14}]

p data.grep(age: (1..20))
# => [{:id=>1, :name=>"Homu", :age=>13}, {:id=>2, :name=>"mami", :age=>14}, {:id=>4, :name=>"saya", :age=>14}]
```


## 補足1: `==` ではなくて `===` で比較する理由

* `===` を使用することでより細かい・抽象的な条件を指定することが出来る
  * `Class` や `Regexp`、`Proc` などで比較することが出来る
* 内部で `===` を使用している場合、 `==` で比較したい場合は `obj.method(:==)` を渡せば実現出来るが、その逆は出来ない
  * 内部で `==` を使用している場合、 `===` で比較ししたくても出来ない


## 補足2: 空のハッシュの比較に関して

* `Object#===` の場合だと `{} === 42` が例外ではなくて `false` を返していたので、`Hash#===` も `false` を返すようにした
  * `{} === {}` が `true` を返すのも同様の理由になります
  * これにより以下のような既存のコードも互換性を壊すことなく動作するかと思います

```ruby
def check n
	case n
	when {}
		"Empty Hash"
	when []
		"Empty Array"
	when 0
		"Zero"
	else
		"Not empty"
	end
end

p check({})   # => "Empty Hash"
p check([])   # => "Empty Array"
p check(0)    # => "Zero"
p check({ name: "mado" })   # => "Not empty"
```

以上、`Hash#===` に関する提案になります。
挙動に関して疑問点や意見などございましたらコメント頂けると助かります。


---Files--------------------------------
hash_eqq.patch (2.36 KB)
hash_eqq.patch (2.69 KB)


-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>

In This Thread

Prev Next