この記事はRuby Advent Calendar 2014 – Qiitaの12/22エントリです。
$ ruby -v
ruby 2.1.5p273
です。
「Rubyの中の仕組みをしりたいけど、C言語をがっつり読むのはちょっと・・・」というみなさん、Rubyのしくみ -Ruby Under a Microscopeは読みましたでしょうか?
現在5章の途中まで読みました。なかなか面白い。
前半はVMの話が中心となっています。今回、Rubyのメソッド呼び出し周辺を少し調べてみたので、オプション引数とブロック引数について記事を書いてみます。
省略可能な引数(オプション引数)
デフォルト値を指定した引数のことです。
デフォルト引数に関する記事を読んで"ぐぬぬ"とうなったり、自分でもこういうコードを書いて遊んだりしています。
def test(a = 10, b = a + 1)
p [a, b]
end
test()
#=> [10, 11]
test(20)
#=> [20, 21]
class MyClass
def foo
"foo"
end
def bar(str = foo)
p str
end
end
MyClass.new.bar("bar")
#=> "bar"
MyClass.new.bar
#=> "foo"
これってどういう仕組みでメソッドの呼び出しをしているのでしょうか?("Rubyのしくみ"ではa = 10
のようなパターンを説明しています)
"Rubyのしくみ"はこう言っている
"Rubyのしくみ"にはこう書いてあります。
デフォルト値の設定されたオプション引数を使った場合、YARVは対象のメソッドに追加のコードを仕込み、そこで引数にデフォルト値を設定する。メソッドが呼び出された際、もしオプション引数に値が設定されていれば、YARVはプログラムカウンタ(PCレジスタ)をリセットして、コンパイル時に追加で仕込んだコードは無視して処理を進める。(P.106)
それではオプション引数にメソッドを指定したときの挙動を確認してみましょう。
code = <<STR
def foo
"foo"
end
def bar(str = foo)
p str
end
bar()
STR
puts RubyVM::InstructionSequence.compile(code).disasm
== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
0000 trace 1 ( 1)
0002 putspecialobject 1
0004 putspecialobject 2
0006 putobject :foo
0008 putiseq foo
0010 opt_send_simple <callinfo!mid:core#define_method, argc:3, ARGS_SKIP>
0012 pop
0013 trace 1 ( 5)
0015 putspecialobject 1
0017 putspecialobject 2
0019 putobject :bar
0021 putiseq bar
0023 opt_send_simple <callinfo!mid:core#define_method, argc:3, ARGS_SKIP>
0025 pop
0026 trace 1 ( 9)
0028 putself
0029 opt_send_simple <callinfo!mid:bar, argc:0, FCALL|ARGS_SKIP>
0031 leave
== disasm: <RubyVM::InstructionSequence:foo@<compiled>>=================
0000 trace 8 ( 1)
0002 trace 1 ( 2)
0004 putstring "foo"
0006 trace 16 ( 3)
0008 leave ( 2)
== disasm: <RubyVM::InstructionSequence:bar@<compiled>>=================
local table (size: 2, argc: 0 [opts: 2, rest: -1, post: 0, block: -1, keyword: 0@3] s0)
[ 2] str<Opt=0>
0000 putself ( 5)
0001 opt_send_simple <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SKIP>
0003 setlocal_OP__WC__0 2
0005 trace 8
0007 trace 1 ( 6)
0009 putself
0010 getlocal_OP__WC__0 2
0012 opt_send_simple <callinfo!mid:p, argc:1, FCALL|ARGS_SKIP>
0014 trace 16 ( 7)
0016 leave ( 6)
コンパイル結果で注目すべきは最後の塊です。これはbar
メソッドの挙動を表しています。
[ 2] str<Opt=0>
0000 putself ( 5)
0001 opt_send_simple <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SKIP>
0003 setlocal_OP__WC__0 2
この抜粋した箇所が、オプション引数にあたる部分です。self
をスタックにのせて、foo
メソッドを呼びだします。その結果をstr
ローカル変数にセットします。このようなYARV命令列が作成されるため、メソッド呼び出しの結果を、オプション引数のデフォルト値にすることもできるのです。
ブロック引数
リファレンスマニュアルによると、ブロックとはdo ... end または { ... } で囲まれたコードの断片
のことで、制御構造の抽象化のために用いられるそうです。
["ruby", "python", "perl"].map do |lang|
lang.capitalize
end
#=> ["Ruby", "Python", "Perl"]
このコードは次のようにも書けます。
["ruby", "python", "perl"].map &:capitalize
#=> ["Ruby", "Python", "Perl"]
では&:capitalize
とはいったい何なのでしょう?
メソッド呼び出し時の&
の役割
リファレンスマニュアルに書いてあるとおり、メソッド呼び出し時に&
のついた引数を渡すと、その引数はブロック引数として扱われます。
コードで確認してみましょう(3、5のコードは比較のためのコードで、標準の状態ではArgumentError
が発生します)。
require "pp"
comp = -> code, i do
puts "\n#{i+1}). #{code}"
puts RubyVM::InstructionSequence.compile(code).disasm
end
codes = []
# (1)
codes << <<STR
["a", "b", "c"].map
STR
# (2)
codes << <<STR
["a", "b", "c"].map {|str| str.upcase }
STR
# (3)
codes << <<STR
["a", "b", "c"].map :upcase
STR
# (4)
codes << <<STR
["a", "b", "c"].map &:upcase
STR
# (5)
codes << <<STR
["a", "b", "c"].map :a, &:upcase
STR
codes.each_with_index {|code, i| comp[code, i]}
コンパイル結果の抜粋をみてみましょう。
1). ["a", "b", "c"].map
------------------------------------------------------------------------
0000 trace 1 ( 1)
0002 putstring "a"
0004 putstring "b"
0006 putstring "c"
0008 newarray 3
0010 opt_send_simple <callinfo!mid:map, argc:0, ARGS_SKIP>
0012 leave
2). ["a", "b", "c"].map {|str| str.upcase }
------------------------------------------------------------------------
0000 trace 1 ( 1)
...
0010 send <callinfo!mid:map, argc:0, block:block in <compiled>>
0012 leave
...
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s3)
[ 2] str<Arg>
0000 trace 256 ( 1)
0002 trace 1
0004 getlocal_OP__WC__0 2
0006 opt_send_simple <callinfo!mid:upcase, argc:0, ARGS_SKIP>
0008 trace 512
0010 leave
3). ["a", "b", "c"].map :upcase
------------------------------------------------------------------------
0000 trace 1 ( 1)
...
0010 putobject :upcase
0012 opt_send_simple <callinfo!mid:map, argc:1, ARGS_SKIP>
0014 leave
4). ["a", "b", "c"].map &:upcase
------------------------------------------------------------------------
0000 trace 1 ( 1)
...
0010 putobject :upcase
0012 send <callinfo!mid:map, argc:0, ARGS_BLOCKARG>
0014 leave
5). ["a", "b", "c"].map :a, &:upcase
------------------------------------------------------------------------
0000 trace 1 ( 1)
...
0010 putobject :a
0012 putobject :upcase
0014 send <callinfo!mid:map, argc:1, ARGS_BLOCKARG>
0016 leave
(3)、(4)、(5)から分かるとおり、&:upcase
と書いたときはargc
としてはカウントされず、ARGS_BLOCKARG
として扱われていることが分かります。
(2)、(4)をみると、{}
の形でブロックを渡したときと&
の形でブロックを渡したときとで、内部でのブロックの持ち方が異なることが分かります。
iseq.c
をみてみるとrb_call_info_t->flag
がVM_CALL_ARGS_BLOCKARG
を含んでいる状態のようですね。
しかしこれだけでは、メソッド呼び出し時にはブロックではなく、シンボルでしかありません(きっと)。どこかでシンボルをうまいことブロック(というかProcオブジェクト)に変換しているはずです。ここでYARV命令の定義ファイルをみてみると、send
命令内でvm_caller_setup_args
を呼んでいることが分かります。
DEFINE_INSN
send
(CALL_INFO ci)
(...)
(VALUE val) // inc += - (int)(ci->orig_argc + ((ci->flag & VM_CALL_ARGS_BLOCKARG) ? 1 : 0));
{
ci->argc = ci->orig_argc;
ci->blockptr = 0;
vm_caller_setup_args(th, reg_cfp, ci);
vm_search_method(ci, ci->recv = TOPN(ci->argc));
CALL_METHOD(ci);
}
vm_insnhelper.c
をみてみると、vm_caller_setup_args
内で
-
VM_CALL_ARGS_BLOCKARG
フラグがたっているなら - SPの一つもどしてproc変数に代入して
- proc変数の中身がPrcoオブジェクトでないなら
to_proc
で変換して - 変換できないときは例外を発生させる
このようにしてProcオブジェクトへの暗黙の変換を行ってから、メソッドの呼び出しを行っています。
vm_caller_setup_args(const rb_thread_t *th, rb_control_frame_t *cfp, rb_call_info_t *ci)
{
...
if (UNLIKELY(ci->flag & VM_CALL_ARGS_BLOCKARG)) {
rb_proc_t *po;
VALUE proc;
proc = *(--cfp->sp);
if (proc != Qnil) {
if (!rb_obj_is_proc(proc)) {
VALUE b;
SAVE_RESTORE_CI(b = rb_check_convert_type(proc, T_DATA, "Proc", "to_proc"), ci);
if (NIL_P(b) || !rb_obj_is_proc(b)) {
rb_raise(rb_eTypeError,
"wrong argument type %s (expected Proc)",
rb_obj_classname(proc));
}
proc = b;
}
GetProcPtr(proc, po);
ci->blockptr = &po->block;
RUBY_VM_GET_BLOCK_PTR_IN_CFP(cfp)->proc = proc;
}
}
else if (ci->blockiseq != 0) { /* likely */
...
}
Symbol#to_proc
Symbol#to_proc
で生成されたProcオブジェクトをcallすると、callの第一引数をレシーバ、第二引数以降を引数としてself(symbolオブジェクトのこと)という名前のメソッドを呼び出します。
:to_i.to_proc["ff", 16]
#=> 255 *これは"ff".to_i(16)と同じです
ちなみに引数なしでcallするとレシーバがないというArgumentError
が発生します。
:to_i.to_proc[]
ArgumentError: no receiver given
そのため
["a", "b", "c"].map &:upcase
というコードは、"第一引数をレシーバとして、upcaseメソッドを呼び出す"Procオブジェクトを生成し、"a"、"b"、"c"をそのProcオブジェクトに適用していることになります。
おまけ
&
はシンボル以外にも利用できます。オブジェクトを用いた例はリファレンスマニュアルに書いてありますので、ここでは変数やメソッドを利用した例をあげます。
メソッドを利用した例:
def make_upcase_lambda
-> a { a.upcase }
end
p ["Aa", "Bb", "Cc"].map &make_upcase_lambda
#=> ["AA", "BB", "CC"]
変数を利用した例:
downcase_lambda = -> a { a.downcase }
p ["Aa", "Bb", "Cc"].map &downcase_lambda
#=> ["aa", "bb", "cc"]
lambdaを利用した例:
p ["Aa", "Bb", "Cc"].map &-> a { a.downcase }
#=> ["aa", "bb", "cc"]
まとめ
"Rubyのしくみ -Ruby Under a Microscope"って面白いですね(小並感)。後半戦はblock
やbinding
も登場するようで、わくわくしますね。
明後日はRailsのAdvent Calendarを書かなくては・・・