LoginSignup
45
39

More than 5 years have passed since last update.

Rubyの引数(オプション引数とブロック引数)について

Posted at

この記事は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->flagVM_CALL_ARGS_BLOCKARGを含んでいる状態のようですね。

しかしこれだけでは、メソッド呼び出し時にはブロックではなく、シンボルでしかありません(きっと)。どこかでシンボルをうまいことブロック(というかProcオブジェクト)に変換しているはずです。ここでYARV命令の定義ファイルをみてみると、send命令内でvm_caller_setup_argsを呼んでいることが分かります。

insns.def
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_insnhelper.c
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"って面白いですね(小並感)。後半戦はblockbindingも登場するようで、わくわくしますね。

明後日はRailsのAdvent Calendarを書かなくては・・・

45
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
39