前回 [http://www.koooge.com/entry/2016/12/22/013733] に続いてrb_ary_select_bang()を読んでいきます。 忘れた方も多いと思いますが**Array#select!**のことです。
rb_ary_select_bang()を読む static VALUE rb_ary_select_bang(VALUE ary) { struct select_bang_arg args;
RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length);
rb_ary_modify(ary);
args.ary = ary;
args.len[0] = args.len[1] = 0;
return rb_ensure(select_bang_i, (VALUE)&args, select_bang_ensure, (VALUE)&args);
}
これ、まず驚くのはごく短い実装だということ。謎の処理がいくつかありますが、ここでは全て無視して最後のreturn rb_ensure() に着目します。どうやらここでselect_bang_ensure関数へのポインタをrb_ensure()に渡していることが分かります。
rb_ensure()とは? rb_ensureを調べます。どうやらarray.cの中には定義はないようです。ではどこにあるのでしょう?doc/extension.rdocまたは doc/extension.ja.rdocに答えがありました。
VALUE rb_ensure(VALUE (*func1)(ANYARGS), VALUE arg1, VALUE (*func2)(ANYARGS), VALUE arg2)
関数func1をarg1を引数として実行し, 実行終了後(たとえ例外が 発生しても) func2をarg2を引数として実行する. 戻り値はfunc1 の戻り値である(例外が発生した時は戻らない).
(´・ω・`)え?つまりselect_bang_i(&args)を実行してから、select_bang_ensure(&args) するの?なんでそんなことするの?
5分程悩みましたが、説明から察するに、例外発生時でも実行される処理を書きたかったのですかね。begin rescure ensureみたいな。 だからrb_ensureなのか!
この実装はeval.cにありました。
VALUE rb_ensure(VALUE (*b_proc)(ANYARGS), VALUE data1, VALUE (e_proc)(ANYARGS), VALUE data2) { / snip */ if ((state = EXEC_TAG()) == 0) { result = (b_proc) (data1); // b_proc実行 } / snip */ (ensure_list.entry.e_proc)(ensure_list.entry.data2); // e_proc実行 / snip */ return result; }
オッケー。 関数ポインタのはずの第1引数と第3引数が、ANYARGSという謎マクロとなっているので一旦置いとこう(;´Д`) ですが例外処理のensureで間違いなさそうです。
selecl_bang_i()を読む 気を取り直して、select_bang_i を読むことで何か掴めるかもしれないので読んでみます。#13053読むためには追わなくて良い気がしますが、OSS読むときってこういうyac shavingが捗りますよね・・。
array.cに定義されています。
static VALUE select_bang_i(VALUE a) { volatile struct select_bang_arg *arg = (void *)a; VALUE ary = arg->ary; long i1, i2;
for (i1 = i2 = 0; i1 < RARRAY_LEN(ary); arg->len[0] = ++i1) { // i1, i2を0からary.lengthまで。
VALUE v = RARRAY_AREF(ary, i1);
if (!RTEST(rb_yield(v))) continue;
if (i1 != i2) {
rb_ary_store(ary, i2, v);
}
arg->len[1] = ++i2;
}
return (i1 == i2) ? Qnil : ary;
}
なんだかいよいよアルゴリズム的な処理が出てきました。 関数名からselect!関連であることは間違いないのでしょうが、難しそうです。
新しいマクロRARRAY_AREF、RTEST、他関数が出てきたので先に見ます。
- まずRARRAY_AREF、これはaryからi1番目の値を取得するマクロのようです。
- 次にRTEST、これはrubyっぽい真偽判定マクロのようです。つまりnilとfalse以外は真、0も真です。
- そしてrb_yieldは、yieldのC実装でvはブロックの引数。
- 最後にrb_ary_store(ary, i2, v)ですが、ary[i2] = vということみたいです。
ここまで調べて気づきましたが、下docs.ruby-lang.org(( https://docs.ruby-lang.org/ja/latest/function/index.html))がリファレンスとして優秀です。 さらに脱線しますが、Ruby Arrayの略でRARRAYみたいですね。ラレーって読んでました(;´Д`) CRubyでは、Rとかrとかrbっていうprefixをつける流儀みたいです。
さて、この処理のi1とi2を何に使っているか読んでいきます。説明のために次のrubyコードを考えてみます。
array = [3, 1, 5, 4, 2] array.select! {|v| v.even? } p array #=> [4, 2]
arrayのうち偶数でない物を取り除く処理です。 それではselect_bang_i()をみていきます。RARRAY_LEN(ary)は5です。i1は0..4の5周イテレートされます。
1週目だけ見てみる [3, 1, 5, 4, 2].select!を実行したとすると、一週目はi1=0, i2=0です。 v = RARRAY_AREF(ary, i1)によって最初のブロック(というか値)の3へのアドレスが渡されます。 rb_yield(&3)によってブロックが実行されます。待ってブロックはどこだ(;´Д)?ary.select! {|v| } っていう感じに、意味ない事になってないですか?どこにいつブロックの処理v.even?`が渡されたか不明です。 これは宿題に・・・。
仕方ないので、ここでrubyのコードを振り返ってみます。 なんとなくですが、if (!RTEST(rb_yield(v))) continue;でcontinueするのは、条件に合わないときと予想します。 rb_ary_store()されるのは条件にあったとき、すなわち[2, 4]が残る。となれば予想はselect!の処理に合致しそうですね。 そしてarg->len[1]というのが、新しいaryの長さ。すなわち2となり、len[0]は元の配列長さ5になるので、辻褄が合いました。
まとめ arg->len[0]とarg->len[1]が代入されているのはselect_bang_i()で、 それぞれ、len[0]は元の配列の長さ、len[1]はselect!後の配列の長さということが分かりました。 変数名から分かりにくいですね・・。
lenの正体が分かったところで、次回select_bang_ensure()に戻って処理を追っていきたいと思います。
スポンサーリンク
[asin:4774158798:detail]
次回 Rubyの#13053を読んでみる(3) [http://www.koooge.com/entry/2016/12/28/011605]