以前からずっと疑問に思っていた事があった。
ruby の後置 if/unless で条件が偽になった場合でも代入構文が実行されるのはどうしてだろう
例えば以下のコードを irb や pry で実行してみて欲しい。
a = 1 if false
続けて a をタイプする。すると nil が表示される。
僕のこれまでの理解だと後置if/unlessは、ステートメントに作用するのでそのステートメント自体が無効になる、つまり代入自体されなかった事になるという理解だった。ruby のパーサのソースコードを見ても後置ifはステートメントに作用している様だった。
        | stmt modifier_if expr_value
            {
            /*%%%*/
            $$ = new_if($3, remove_begin($1), 0);
            fixpos($$, $3);
            /*%
            $$ = dispatch2(if_mod, $3, $1);
            %*/
            }
だって raise "foo" if false で例外が飛ばないなら、代入もされないでしょと思っていたけど nil が代入される。この ruby のコードを AST ダンプするとこうなる。
# @ NODE_SCOPE (line: 1)
# +- nd_tbl: :a
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_PRELUDE (line: 1)
#     +- nd_head:
#     |   (null node)
#     +- nd_body:
#     |   @ NODE_IF (line: 1)
#     |   +- nd_cond:
#     |   |   @ NODE_FALSE (line: 1)
#     |   +- nd_body:
#     |   |   @ NODE_DASGN_CURR (line: 1)
#     |   |   +- nd_vid: :a
#     |   |   +- nd_value:
#     |   |       @ NODE_LIT (line: 1)
#     |   |       +- nd_lit: 1
#     |   +- nd_else:
#     |       (null node)
#     +- nd_compile_option:
#         +- coverage_enabled: true
これを見ると、AST に落とし込んだ時点でノードテーブルに a が現れている。つまり、後置ifが偽であろうとも代入構文を認識しているという事になる。この AST をどう walk しても代入構文には到達しないよなーと悩んでソースを見たりしたけど良く分からなかったので Matz に直接聞いた。
@yukihiro_matz 飛ばされるのが正しいと思ったのですが実際は nil が入ります。--dump=parsetree を見る限り AST の時点で nd_tbl がアサインされてるっぽいのですが、これは期待した動作でしょうか?(続
— Vim芸人 (@mattn_jp) January 31, 2017
@yukihiro_matz また先の後置if/unlessはどう解釈されるべきでしょうか?
— Vim芸人 (@mattn_jp) January 31, 2017
@mattn_jp 「変数宣言」は構文解析時に代入が存在しているかどうかで行われ、実行は関係ありません。ifなどでスキップしても変数は(nilで初期化されて)存在するということです。
— Yukihiro Matsumoto (@yukihiro_matz) January 31, 2017
@mattn_jp 構文解析的にはその手順ですね。
— Yukihiro Matsumoto (@yukihiro_matz) January 31, 2017
つまり ruby はこの stmt modifier_if expr_value を見つけると、まずステートメントが何かを判定して代入構文であれば変数 a を用意し、そのあと後置ifを判定して最後に代入という動きを取る。もちろん後置ifが偽であれば代入はされない。よって a は初期化されたままの nil が格納されるという事になる。これを理解した上で以下を見ると、なぜ NameError: undefined local variable or method `a' for main:Object ではなく NoMethodError: undefined method `+' for nil:NilClass なのか理解できた。なるほど深い。
irb(main):001:0> a = a + 1
NoMethodError: undefined method `+' for nil:NilClass
        from (irb):1
        from c:/msys64/mingw64/bin/irb.cmd:19:in `<main>'
irb(main):002:0>
 
