ruby は メソッドに return が不要なのではなく、元々そうなっているだけ

皆さん、こんにちは。MUGENUP の osada です。 いきなりですが、問題です。

def if_expression(flag)
  if flag
    "NG"
  else
    "OK"
  end
end

Ruby最後に評価された値が返る と言われていますね。 では、上記のメソッド

if_expression false

の返り値は、何ですか?






はい、正解です。OKが返ってきますね。

ちょっと長いので、リファクタリングしましょう。

def if_modifier(flag)
  "OK"
  "NG" if flag
end

さてもう一度。

if_modifier(false)

の返り値は、何ですか? Ruby最後に評価された値が返る と言われていましたよね?






はい。正解です。nil が返ってきますね。

え、"OK"じゃないのかって?

いいえ、後置if は if とは全く違います

右辺の条件が成立する時に、左辺の式を評価してその結果を返します。 条件が成立しなければ nil を返します。 (http://docs.ruby-lang.org/ja/2.1.0/doc/spec=2fcontrol.html)

という流れでreturn を書けばこんな混乱しないのにという話がでました。

いやいや、return とか不要じゃない?という立場でこの記事を書いています。

ruby最後に評価される値 が返る、と言われますが、それはどのような意味なのでしょうか?

そんな本日は ruby の挙動についてのお話です。 ターゲットは、アセンブラに興味がある人です(アセンブラを使いこなす人は、対象外でお願いします (笑))

if と、if 修飾子 (後置 if) の違い

まず if と if 修飾子を使ったコードを書きます。

# return_if.rb

def if_expression(flag)
  if flag
    "NG"
  else
    "OK"
  end
end

def if_modifier(flag)
  "OK"
  "NG" if flag
end

そしておもむろに、ディスアセンブラを書きます(!)。

# disasm.rb
iseq = RubyVM::InstructionSequence.compile_file ARGV.first, false
print iseq.disasm

さきほどのファイルを食わせます

[master]~/projects/ruby_test/practice/if_expression: ruby disasem.rb return_if.rb
== disasm: <RubyVM::InstructionSequence:<main>@return_if.rb>============
0000 putspecialobject 1                                               (  20)
0002 putspecialobject 2
0004 putobject        :if_expression
0006 putiseq          if_expression
0008 send             <callinfo!mid:core#define_method, argc:3, ARGS_SKIP>
0010 pop
0011 putspecialobject 1                                               (  28)
0013 putspecialobject 2
0015 putobject        :if_modifier
0017 putiseq          if_modifier
0019 send             <callinfo!mid:core#define_method, argc:3, ARGS_SKIP>
0021 leave
== disasm: <RubyVM::InstructionSequence:if_expression@return_if.rb>=====
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
[ 2] flag<Arg>
0000 getlocal         flag, 0                                         (  21)
0003 branchunless     11
0005 jump             7
0007 putstring        "NG"                                            (  22)
0009 jump             13                                              (  21)
0011 putstring        "OK"                                            (  24)
0013 leave
== disasm: <RubyVM::InstructionSequence:if_modifier@return_if.rb>=======
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
[ 2] flag<Arg>
0000 getlocal         flag, 0                                         (  30)
0003 branchunless     11
0005 jump             7
0007 putstring        "NG"
0009 jump             12
0011 putnil
0012 leave

初めの ブロックはメソッド定義なので飛ばしまずが、 なんとなく気分はわかっていただけると思います。

2番目の if_expressonブロックと、 3番目の if_modifier ブロックを見ていくことにしましょう。

if_expression

2番めのブロックは、if_expression のコードです。

== disasm: <RubyVM::InstructionSequence:if_expression@return_if.rb>=====
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
[ 2] flag<Arg>
0000 getlocal         flag, 0                                         (  21)
0003 branchunless     11
0005 jump             7
0007 putstring        "NG"                                            (  22)
0009 jump             13                                              (  21)
0011 putstring        "OK"                                            (  24)
0013 leave
  • 0番でflag 変数から値をとりだします。
  • 3番で、それが nil, false でなければ、11番に飛びます。

if なのに、branchunless というのは面白いですね。 これは、否定を確認した方が速いからでしょう。

flagfalse のとき、

  • 11番は、"OK"を スタックに積みます。
  • 13番で leave します。

flagtrue のとき、

  • 5番で、jump 7します。
  • 7番で "NG" を スタックに積みます。
  • 9番で jump 13 します。
  • 13番で leave します。

if_modifier

3番めのブロックは、if_modifier のコードです。

== disasm: <RubyVM::InstructionSequence:if_modifier@return_if.rb>=======
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
[ 2] flag<Arg>
0000 getlocal         flag, 0                                         (  30)
0003 branchunless     11
0005 jump             7
0007 putstring        "NG"
0009 jump             12
0011 putnil
0012 leave
  • 0番でflag 変数から値をとりだします。
  • 3番で、それが nil, false でなければ、11番に飛びます。
  • 11番で putnilnil をスタックに積みます
  • 12番で lreave します。

はい、成立しないときは、nil が返りますね。仕様ですから仕方ないですよね。

あれ? "OK" は、どこに行ったのでしょう? putstring すらないとか、かわいそうですね。

return が不要な理由

ruby に return が不要な理由は、もうお分かりですね。

というかそもそも、return ではなくleaveですね。 ruby としては、メソッドの元から去る(leave)だけで、 値を返し(return)ているつもりはなさそうです。

return は単に、メソッドから去るタイミングを指定しているだけなので、 最後に書く必要はない、ということなのでしょう。

あれ?そんなのどの言語も一緒じゃん! と思われません?

ではここで、LL界の委員長 python にご出座いただきましょう。

python は return が必須です!

pythonは return が必須です。

def if_expression(flag):
    if flag:
        return "NG"
    else:
        return "OK"

import dis

dis.dis(if_expression)
[master]~/projects/python_test: python if_expression.py
  2           0 LOAD_FAST                0 (flag)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 ('NG')
              9 RETURN_VALUE

  5     >>   10 LOAD_CONST               2 ('OK')
             13 RETURN_VALUE
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE

左端はソース行なので、無視してください。

  • 0番で flag から ロードします
  • 3番で、それが FALSE なら、 10番にジャンプします

これも、falseをチェックするのはお決まりですね。

flagtrue のとき、

  • 6番で "NG" をスタックに積みます
  • 9番で、RETURN_VALUEします( return だよ!やったね!)

flagtrue のとき、

  • 10番で、"OK" をスタックに積みます。
  • 13番で、RETURN_VALUEします

あれ?2行残っていますね。

  • 14番で、Noneを スタックに積みます。
  • 17番で、RETURN_VALUEします

これは、python の関数が、return がないときはNoneを返す という仕様になっているからです。

pythonインデントがブロックを表すので、 この関数自体の返り値が書かれていません。 よって、最後に None を返すコードが暗黙的に追加されています。

さて、動作はrubyとほとんど同じですね。

では、return を書かないとどうなるか見てみましょう。

def if_expression(flag):
    if flag:
        "OK"
    else:
        "NG"

import dis
dis.dis(if_expression)
[master]~/projects/python_test: python if_expression.py
 17           0 LOAD_FAST                0 (flag)
              3 POP_JUMP_IF_FALSE        9

 18           6 JUMP_FORWARD             0 (to 9)

 20     >>    9 LOAD_CONST               0 (None)
             12 RETURN_VALUE

何もない……。

スタックに積んだ物が返る、というのはどの言語でも一緒ですが、 return をどのように取り扱うかで、コードが異なる、ということですね。

まとめ

  • if 修飾子 は 成立しないとき nil を返します
  • メソッドは最後にスタックに積まれた値を返します
  • RubyVM::InstructionSequenceVM のコードが読めます
  • ifbranchunless を判定します
  • rubyreturn は、メソッドの返りのタイミングの指定です。
  • pythonreturn は、関数の返り値の指定です。

ということで、ruby にとって、returnメソッドの返り値の指定ではなく、 どのタイミングでメソッドからleaveするかの指定、という意味なので、 実質returnが不要になる、ということのようです。

さて、rubyreturn 必要ですか

メソッドの値を返す場合は、必ずreturnを使用する。

Rubyコーディング規約

それと return なしのほうがちょっとだけ効率もいい。 それはなぜかというと RubyHackingGuide でも見てくれればわかるのだが、 return は結局 longjmp だからだ。

LoveRubyNet Wiki: RubyCodingStyle

Avoid return where not required.

styleguide/RUBY-STYLE at master · chneukirchen/styleguide

……あれ〜??

(おまけ) 人生、宇宙、すべての答え

ruby の ソースを追いかけていて、人生の答えを知りました。 さすが ruby ですね。

/**
  @c joke
  @e The Answer to Life, the Universe, and Everything
  @j 人生、宇宙、すべての答え。
 */
DEFINE_INSN
answer
()
()
(VALUE ret)
{
    ret = INT2FIX(42);
}

http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/insns.def?view=markup

参考