Arrayにメソッドを追加して遊んでみる - ore-public/ruby GitHub Wiki
array.cのrb_ary_firstの実装を参考に書いてみる。
https://github.com/ore-public/ruby/commit/07edb808c9f0b141046b5ef1afc80c86eb6d7473
% autoconf
% ./configure --prefix=$HOME/.rbenv/versions/array_second
% make
% make install
% rbenv shell array_second
% irb
irb(main):001:0> [1,2].second
=> 2
irb(main):002:0> [1].second
=> nil
irb(main):003:0>
だいたい出来てる。面白い。
Array#firstは引数を0 or 1個取るメソッド。2個以上のメソッドを渡すとArgumentErrorとなる。
irb(main):007:0> [1].first(1,2,3,4,5)
ArgumentError: wrong number of arguments (5 for 1)
from (irb):7:in `first'
from (irb):7
from ./bin/irb:11:in `<main>'
Array#secondは引数をいくら渡してもエラーにならない。
irb(main):006:0> [1].second(1,2,3,4,5)
=> nil
ArgumentErrorはどこで、どう実装されているのか調べてみる。 Array#secondは引数無しの仕様ということにするので、一つ以上引数を渡したら同じようにエラーを出したい。
Array#firstの実装を見る。
/* array.c */
static VALUE
rb_ary_first(int argc, VALUE *argv, VALUE ary)
{
if (argc == 0) {
if (RARRAY_LEN(ary) == 0) return Qnil;
return RARRAY_AREF(ary, 0);
}
else {
return ary_take_first_or_last(argc, argv, ary, ARY_TAKE_FIRST);
}
}
引数がある時は、ary_take_first_or_lastが呼ばれている。 そっちの実装を探してみる。
/* array.c */
static VALUE
ary_take_first_or_last(int argc, const VALUE *argv, VALUE ary, enum ary_take_pos_flags last)
{
VALUE nv;
long n;
long len;
long offset = 0;
rb_scan_args(argc, argv, "1", &nv);
n = NUM2LONG(nv);
len = RARRAY_LEN(ary);
if (n > len) {
n = len;
}
else if (n < 0) {
rb_raise(rb_eArgError, "negative array size");
}
if (last) {
offset = len - n;
}
return ary_make_partial(ary, rb_cArray, offset, n);
}
rb_scan_argsって関数が名前的に怪しい気がする。
http://docs.ruby-lang.org/ja/2.2.0/function/rb_scan_args.html
このドキュメント見た感じ、やっぱりこれが該当っぽい。
https://github.com/ore-public/ruby/commit/3e3f8de72fbeb04d34c680a56bc56f75450b25b2
これで、ビルドして動かしてみる。
irb(main):003:0> [1,2].second(1)
ArgumentError: wrong number of arguments (1 for 0)
from (irb):3:in `second'
from (irb):3
from ./bin/irb:11:in `<main>'
OK!
具体的にどういう処理をしているのか気になるの
・・・ちょっと長かった。
実装を追いかける前に、どういう処理なのかもう少し詳しくみる。明らかに引数の数をチェックだけしてる関数ではない。
/* class.c */
int
rb_scan_args(int argc, const VALUE *argv, const char *fmt, ...)
{
int i;
const char *p = fmt;
VALUE *var;
va_list vargs;
int f_var = 0, f_hash = 0, f_block = 0;
int n_lead = 0, n_opt = 0, n_trail = 0, n_mand;
int argi = 0;
VALUE hash = Qnil;
if (ISDIGIT(*p)) {
n_lead = *p - '0';
p++;
if (ISDIGIT(*p)) {
n_opt = *p - '0';
p++;
if (ISDIGIT(*p)) {
n_trail = *p - '0';
p++;
goto block_arg;
}
}
}
if (*p == '*') {
f_var = 1;
p++;
if (ISDIGIT(*p)) {
n_trail = *p - '0';
p++;
}
}
block_arg:
if (*p == ':') {
f_hash = 1;
p++;
}
if (*p == '&') {
f_block = 1;
p++;
}
if (*p != '\0') {
rb_fatal("bad scan arg format: %s", fmt);
}
n_mand = n_lead + n_trail;
if (argc < n_mand)
goto argc_error;
va_start(vargs, fmt);
/* capture an option hash - phase 1: pop */
if (f_hash && n_mand < argc) {
VALUE last = argv[argc - 1];
if (NIL_P(last)) {
/* nil is taken as an empty option hash only if it is not
ambiguous; i.e. '*' is not specified and arguments are
given more than sufficient */
if (!f_var && n_mand + n_opt < argc)
argc--;
}
else {
hash = rb_check_hash_type(last);
if (!NIL_P(hash)) {
VALUE opts = rb_extract_keywords(&hash);
if (!hash) argc--;
hash = opts ? opts : Qnil;
}
}
}
/* capture leading mandatory arguments */
for (i = n_lead; i-- > 0; ) {
var = va_arg(vargs, VALUE *);
if (var) *var = argv[argi];
argi++;
}
/* capture optional arguments */
for (i = n_opt; i-- > 0; ) {
var = va_arg(vargs, VALUE *);
if (argi < argc - n_trail) {
if (var) *var = argv[argi];
argi++;
}
else {
if (var) *var = Qnil;
}
}
/* capture variable length arguments */
if (f_var) {
int n_var = argc - argi - n_trail;
var = va_arg(vargs, VALUE *);
if (0 < n_var) {
if (var) *var = rb_ary_new4(n_var, &argv[argi]);
argi += n_var;
}
else {
if (var) *var = rb_ary_new();
}
}
/* capture trailing mandatory arguments */
for (i = n_trail; i-- > 0; ) {
var = va_arg(vargs, VALUE *);
if (var) *var = argv[argi];
argi++;
}
/* capture an option hash - phase 2: assignment */
if (f_hash) {
var = va_arg(vargs, VALUE *);
if (var) *var = hash;
}
/* capture iterator block */
if (f_block) {
var = va_arg(vargs, VALUE *);
if (rb_block_given_p()) {
*var = rb_block_proc();
}
else {
*var = Qnil;
}
}
va_end(vargs);
if (argi < argc) {
argc_error:
rb_error_arity(argc, n_mand, f_var ? UNLIMITED_ARGUMENTS : n_mand + n_opt);
}
return argc;
}