Enumerable#all? と Enumerable#any? の実装を読んだ

約3年前にEnumerable#all? が罠っぽかった - tanaken’s blogという記事を書いたのですが、Rubyの実装には踏み込みませんでした。最近気になってRubyの実装を読んだので、その結果を書いておきます。

Enumerable#all? / Enumerable#any? とは

Enumerable#all? (Ruby 3.0.0 リファレンスマニュアル)

すべての要素が真である場合に true を返します。偽である要素があれば、ただちに false を返します。

Enumerable#any? (Ruby 3.0.0 リファレンスマニュアル)

すべての要素が偽である場合に false を返します。真である要素があれば、ただちに true を返します。

Enumerableなオブジェクトに対して、すべての要素が真であるかどうかを調べたい時は all? を、どれか一つでも真の要素があるかを調べたい場合は any? を使うことができます。 個人的な経験では業務コードでも何度かお世話になっているメソッドたちです。

[3, 6, 9].all? {|v| v % 3 == 0 } # => true (すべての要素が3の倍数)

[1, 2, 3].all? {|v| v % 3 == 0 } # => false (3の倍数は3だけ)

[1, 2, 3].any? {|v| v % 3 == 0 } # => true

便利なメソッドですが、要素が空のオブジェクトに対しての挙動に注意が必要です。

[].all? {|v| v % 3 == 0 } # => true

[].any? {|v| v % 3 == 0 } # => false

これを日本語で表現すると

  • 「空配列の要素はすべて3の倍数ですか?」「はい
  • 「空配列の要素のうちどれか1つは3の倍数ですか?」「いいえ

というように読めてしまいます。

個人的な感覚では、一つ目の問いに対して「いいえ」と答えるのが正しいように感じます。

自身の感覚と実際の挙動とのギャップがあったので "罠っぽかった" と題して書いたのが冒頭に紹介した記事です。

今回の記事について

Enumerable#all?Enumerable#any? の挙動がどのような実装によって実現されているのかを知ることがゴールです。

次の4項目について書いていきます。

  • 読むべきコードにたどり着くまでの道のり
  • 雰囲気コードリーディング
  • Enumerable#all? の実装について
  • Enumerable#any? の実装について

読むべきコードにたどり着くまでの道のり

さて、どういう実装になっているか知りたい、という状況なので、https://github.com/ruby/rubyのコードを読めばいいわけです。

が、さっそく迷子になりますよね。普段からruby/rubyのコードを読んでいるわけでもないし、C言語に精通しているわけでもないのでさっぱりです。

そんな時にRubyリファレンスマニュアルが便利です。

Enumerable#all? (Ruby 3.0.0 リファレンスマニュアル)の右上に [rdoc] というリンクがあるのでそれを開きます。

f:id:tanaken0515:20210926174234p:plain

このページ https://docs.ruby-lang.org/en/3.0.0/Enumerable.html#method-i-all-3F が開くはずです。 この画面でメソッド名にマウスカーソルを持っていくと、次のキャプチャのように click to toggle source という表示が出てきます。

f:id:tanaken0515:20210926174523p:plain

クリックすると実装コードをみることができます。

static VALUE
enum_all(int argc, VALUE *argv, VALUE obj)
{
    struct MEMO *memo = MEMO_ENUM_NEW(Qtrue);
    WARN_UNUSED_BLOCK(argc);
    rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo);
    return memo->v1;
}

これだけでもざっくり実装内容が分かります。

さらに、https://github.com/ruby/rubyでどのファイルを読めばよさそうか知りたければ、この実装を元にキーワード検索するとだいたいわかるんじゃないかと思います。

今回は enum_all で検索してみます。

https://github.com/ruby/ruby/search?q=enum_all

f:id:tanaken0515:20210926175158p:plain

enum.c で実装されていることが分かりました。

こんな感じで読むべきコードにたどり着くことができました。

雰囲気コードリーディング

C言語を雰囲気で読んでいきます。(筆者のC言語への理解度は、10年以上前に大学の講義でC言語やったことあるような無いような気がする程度です)

まずは click to toggle source で出てきた箇所のコードを読んでいきましょう。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1530-L1537

static VALUE
enum_all(int argc, VALUE *argv, VALUE obj)
{
    struct MEMO *memo = MEMO_ENUM_NEW(Qtrue);
    WARN_UNUSED_BLOCK(argc);
    rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo);
    return memo->v1;
}

4行目の struct MEMO *memo = MEMO_ENUM_NEW(Qtrue); からは「true的な何かをメモしておくぜ」というような気持ちが窺えますね。

6行目の rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo); からは「いまからrubyのblockを呼び出すぜ?メモの値とENUMFUNC(all)を使ってな!」的な勢いを感じますね。

ENUMFUNC(all) ってなんですか?

enum.c のコードを読んでみると、それっぽい子がいました。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1462

#define ENUMFUNC(name) argc ? name##_eqq : rb_block_given_p() ? name##_iter_i : name##_i

これの name = all ってことなので、なんとなく

  • argc が真なら all_eqq を使いなさい。そうでないなら次へ。
  • rb_block_given_p() が真なら all_iter_i を使いなさい。そうでないなら all_i を使いなさい。

のような感じでしょうか。

all_eqq / all_iter_i / all_i ってなんですか?

これら3つの実装を雰囲気で読んでみると、

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1466-L1489

#define DEFINE_ENUMFUNCS(name) \
static VALUE enum_##name##_func(VALUE result, struct MEMO *memo); \
\
static VALUE \
name##_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo)) \
{ \
    return enum_##name##_func(rb_enum_values_pack(argc, argv), MEMO_CAST(memo)); \
} \
\
static VALUE \
name##_iter_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo)) \
{ \
    return enum_##name##_func(rb_yield_values2(argc, argv), MEMO_CAST(memo));    \
} \
\
static VALUE \
name##_eqq(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo)) \
{ \
    ENUM_WANT_SVALUE(); \
    return enum_##name##_func(rb_funcallv(MEMO_CAST(memo)->v2, id_eqq, 1, &i), MEMO_CAST(memo)); \
} \
\
static VALUE \
enum_##name##_func(VALUE result, struct MEMO *memo)

とありました。DEFINE_ENUMFUNCS(name)name = all を渡すとall_eqq / all_iter_i / all_i が生まれそうな感じがしてきます。

どこかに DEFINE_ENUMFUNCS(all) を呼んでいる場所があるのでしょうか。

DEFINE_ENUMFUNCS(all)

すぐ下にありました。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1497-L1504

DEFINE_ENUMFUNCS(all)
{
    if (!RTEST(result)) {
        MEMO_V1_SET(memo, Qfalse);
        rb_iter_break();
    }
    return Qnil;
}

おおよそ読むべきコードの見当をつけることができましたね。

Enumerable#all? の実装について

改めて enum_all のコードを見てみます。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1530-L1537

static VALUE
enum_all(int argc, VALUE *argv, VALUE obj)
{
    struct MEMO *memo = MEMO_ENUM_NEW(Qtrue);
    WARN_UNUSED_BLOCK(argc);
    rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo);
    return memo->v1;
}

4行目:「true的な何かをメモしておくぜ

6行目:「いまからrubyのblockを呼び出すぜ?メモの値とENUMFUNC(all)を使ってな!

で、ENUMFUNC(all)の実体(?)はDEFINE_ENUMFUNCS(all)で生み出されています。

DEFINE_ENUMFUNCS(all)のコードを読むと、

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1497-L1504

DEFINE_ENUMFUNCS(all)
{
    if (!RTEST(result)) {
        MEMO_V1_SET(memo, Qfalse);
        rb_iter_break();
    }
    return Qnil;
}

もしもRTEST(result)が偽であれば、メモにfalse的な何かをセットしてイテレータをbreakするぜ!」と読めそうです。

これらの内容をまとめると:

  • Enumerable#all?の初期値(?)はtrueである
  • 要素を評価し、結果が偽であるものを発見したらfalseを返す

ということになりますね。

この理解に基づいて、要素が空のケースを考えてみると「要素をひとつも評価していないので、初期値(true)が返ってくる」と解釈することができます。

Enumerable#any? の実装について

読み方は all? の場合と同様です。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1572-L1579

static VALUE
enum_any(int argc, VALUE *argv, VALUE obj)
{
    struct MEMO *memo = MEMO_ENUM_NEW(Qfalse);
    WARN_UNUSED_BLOCK(argc);
    rb_block_call(obj, id_each, 0, 0, ENUMFUNC(any), (VALUE)memo);
    return memo->v1;
}

4行目:「false的な何かをメモしておくぜ

6行目:「いまからrubyのblockを呼び出すぜ?メモの値とENUMFUNC(any)を使ってな!

そしてENUMFUNC(any)

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1539-L1546

DEFINE_ENUMFUNCS(any)
{
    if (RTEST(result)) {
        MEMO_V1_SET(memo, Qtrue);
        rb_iter_break();
    }
    return Qnil;
}

もしもRTEST(result)が真であれば、メモにtrue的な何かをセットしてイテレータをbreakするぜ!」と読めそうです。

これらの内容をまとめると:

  • Enumerable#any?の初期値(?)はfalseである
  • 要素を評価し、結果が真であるものを発見したらtrueを返す

ということになりますね。

この理解に基づいて、要素が空のケースを考えてみると「要素をひとつも評価していないので、初期値(false)が返ってくる」と解釈することができます。

まとめ

Enumerable#all?Enumerable#any? の実装を雰囲気でコードリーディングしてみました。

C言語の知識がなくてもなんとなく読むことができ、理解が深まった感じがします。

冒頭に書いた空配列に対する呼び出しについて、改めて日本語で表現すると

[].all? {|v| v % 3 == 0 } # => true

[].any? {|v| v % 3 == 0 } # => false
  • 「空配列の要素のうち、3の倍数ではない要素は存在しませんでしたか?」「はい(存在しませんでした(だって要素が1つもないからさ))
  • 「空配列の要素のうちどれか1つは3の倍数ですか?」「いいえ

ということになりますね。なるほどな〜。

おしまい。