Ruby の Hash#select, reject のブロックパラメーター


Ruby のハッシュの `select`, `reject` などのメソッドについて，公式リファレンスにも載っていない「あれ？」と思った仕様に気付いたので。

# おさらい

まず，Hash の `select` や `reject` がどんなメソッドなのかを振り返る。

Ruby 1.8 時代は仕様が違ったり，Ruby 2.1 の頃に黒歴史があったりしたが，この記事では最近のバージョンだけを考慮することにする。とりあえず Ruby 2.6 だ。（現時点で公式サポートが継続している Ruby 2.4 以降なら同じだと思う）

Hash には Enumerable が include してあるが，`select` や `reject` は Hash で独自に実装されており，[Enumerable#select](https://docs.ruby-lang.org/ja/2.6.0/method/Enumerable/i/filter.html) や [Enumerable#reject](https://docs.ruby-lang.org/ja/2.6.0/method/Enumerable/i/reject.html) が使われることはない。

`Hash#select` は自身の要素から**特定の条件を満たす要素だけを採用した**新たなハッシュを生成して返すもの。
`Hash#reject` はその論理反転であり，自身の要素から**特定の条件を満たす要素だけを取り除いた**新たなハッシュを生成して返すもの。
いずれも条件はブロックで与える。

以下の例では，条件として，キーが `'ba'` を含み，かつ値が正であるもの，としよう。

```rb
hash = {foo: 3, bar: 4, baz: 0}

p hash.select{ |key, value| (/ba/ =~ key) && (value > 0) }
# => {:bar=>4}

p hash.reject{ |key, value| (/ba/ =~ key) && (value > 0) }
# => {:foo=>3, :baz=>0}
```

これで動作は一目瞭然。

公式リファレンス（Ruby 2.6.0）は以下のとおり。

* [Hash#select](https://docs.ruby-lang.org/ja/2.6.0/method/Hash/i/filter.html)
* [Hash#reject](https://docs.ruby-lang.org/ja/2.6.0/method/Hash/i/reject.html)

`reject` のほうの説明は「self を複製し」とあるが，これは間違いではないかと思う。`select` も `reject` もデフォルト値は引き継がれないし，Hash のサブクラスに適用してもサブクラスでなく Hash を返すから。

なお，ブロックを与えない使い方もあるが，本題に関係ないので割愛。

# ブロックパラメーターは一つでも

ここからが本題。
これらのメソッドは，前節のコード例のように，ブロックパラメーターとしてキーと値の二つを使うのが普通だ。公式リファレンスでもそのような例しか載っていない。

しかし，実はブロックパラメーターを一つだけにする使い方がある。

```rb
hash = {foo: 3, bar: 4, baz: 0}

p hash.select{ |key| /ba/ =~ key }
# => {:bar=>4, :baz=>0}

p hash.reject{ |key| /ba/ =~ key }
# => {:foo=>3}
```

見てのとおり，ブロックパラメーターを一つにした場合，要素のキーが渡ってくる。

これに気付いたとき，「え？」と思った。
ブロックパラメーターが二つであるべきところに一つしか記述しなかったら，配列の形で渡ってくるんじゃないのか？（後述するがこの認識は間違い）
だって，`Hash#each` だったら

```rb
hash = {foo: 3, bar: 4}

hash.each do |a, b|
  p a, b
end
# => :foo
# => 3
# => :bar
# => 4

hash.each do |a|
  p a
end
# => [:foo, 3]
# => [:bar, 4]
```

になるよね？

てことは，`Hash#select` や `Hash#reject` は与えられたブロックのパラメーターの数[^arity]で挙動を変えているのか？（次節で述べるようにこれは誤解）

[^arity]: ブロックが持つブロックパラメーターの数をそのブロックの <ruby>arity<rt>アリティー</ruby> という。一般にコンピューター用語で，関数などの引数が 1 個，2 個，3 個であることを un<strong>ary</strong>, bin<strong>ary</strong>, tern<strong>ary</strong> という。arity はここから作られた単語で，関数などの引数の個数のこと。Ruby ではメソッド側が，与えられたブロックのアリティーを知る手段を持っている。「<ruby>有<rt>あ</ruby>り<ruby>体<rt>てい</ruby>に申せ」ってね。

# 特別な動作ではなかった

結論を先に言えば，ブロックパラメーターを一つだけ指定したときにキーだけが渡ってくるのは，特別な動作でも何でもなかった。だから公式リファレンスにも特に言及はなかったわけだ。

すっかり忘れていたが，メソッドから引数二つで `yield` したとき，ブロックパラメーターが一つしかなかったら，`yield` の最初の引数だけが渡され，二つ目は無視されるのだった。

つまりこういうこと：

```rb
def foo
  yield 1, 2
end

foo{ |x| p x } # => 1
```

これは Ruby の `yield` の仕様であって，特定のクラスとかには関係ない。

ではなぜ，私は配列が渡ってくるはずと勘違いしたのだろうか？
原因は明らかに `Hash#each` の動作を見たためだが，`Hash#each` の動作はどのように理解すればいいのだろうか？

どうやら，`Hash#each` は，キーと値を引数として `yield` しているのではなく，**キーと値からなる配列を引数として `yield` している**らしい。
いや，`Hash#each` は Ruby でなく C で実装しているから，そもそも `yield` なんか呼んでいないのだが，「Ruby コードとして表現すれば」という話である。

Ruby 2.6 の `Hash#each` の実装は↓ここであるらしい。
https://github.com/ruby/ruby/blob/ruby_2_6/hash.c#L2779-L2810

私は C が読めないのでよく分からないが，ぼんやりとは分かる。
ブロックパラメーターの数で場合分けしてあるのは高速化のためのようだが，ともかく 0 個や 1 個の場合は `each_pair_i` が呼ばれる。
`each_pair_i` の実装は↓ここ。
https://github.com/ruby/ruby/blob/ruby_2_6/hash.c#L2762-L2767

ここに

```c
rb_yield(rb_assoc_new(key, value));
```

とある。`rb_assoc_new` というのは引数をつなげた配列（？）を作る関数ぽいので，やはり配列を `yield` しているのだ。

ブロックパラメーターが 2 個以上のときは `each_pair_i_fast` が呼ばれるが，その実装は↓ここ。
https://github.com/ruby/ruby/blob/ruby_2_6/hash.c#L2769-L2777

よく分からないが，こちらも配列を作っているぽい。

Ruby では，配列を `yield` した場合，ブロックパラメーターが 1 個ならその配列がそのまま渡され，2 個なら展開されて渡される，という仕様であった。
つまりこういうこと：

```rb
def foo
  yield [1, 2]
end

foo{ |a| p a }
# => [1, 2]

foo{ |a, b| p a; p b }
# => 1
# => 2
```

これで謎が解けた。

どちらかといえば，与えるブロックパラメーターの数について注意が必要なのは `Hash#each` のほうであったか。