宮水の日記

宮水の日記

主に書評や資格取得について記事を書いています。

Rubyの多重代入でハマって、抽象構文木の出力の仕方が学べた話

最近ハマったのでメモです。

困ったこと

まずは以下のコードをご覧ください。

class HogeHogeService
  def self.call(*a, **k)
    new(*a, **k).tap { |s| s.call }
  end

  def initialize(a_code:, b_code:, c_code:)
    @a_code = a_code,
    @b_code = b_code,
    @c_code = c_code
  end

  attr_reader :a_code, :b_code, :c_code

  def call
    p a_code
  end
end

HogeHogeService.call(a_code: 'a', b_code: 'b', c_code: 'c')
    # => 期待する値: "a"なのに、なぜか["a", "b", "c"]になる

a_codeを参照したとき、"a"だけ出て欲しいのになぜか身に覚えのない配列が表示されて困りました...。





















答え

class HogeHogeService
  def self.call(*a, **k)
    new(*a, **k).tap { |s| s.call }
  end

  def initialize(a_code:, b_code:, c_code:)
    @a_code = a_code  # <=ここにカンマがついていたから!
    @b_code = b_code # <=ここにカンマがついていたから!
    @c_code = c_code
  end

  attr_reader :a_code, :b_code, :c_code

  def call
    p a_code
  end
end

# => "a"
HogeHogeService.call(a_code: 'a', b_code: 'b', c_code: 'c')

Rubyって、こんな風にカンマをつけて変数を定義すると、配列として解釈してくれるんですね。
多重代入の1種だそうです。
docs.ruby-lang.org

a_code = 'a', 'b', 'c'

 # ["a", "b", "c"]
p a_code


今回のコードでも同じパターンの多重代入が行われていました。

a_code = 'a',
b_code = 'b',
c_code = 'c'

# ...!?  b_codeとc_codeの出力どういうことなの...
p a_code #=> ["a", "b", "c"]
p b_code #=>"b"
p c_code #=>"c"

会社の方に教えていただいたのですが、AST(抽象構文木)によると、代入式の左辺にカンマがなく右辺にカンマがある場合、右辺は配列として解釈されるそうです。
RubyのソースのASTを見る方法 - Qiita


全然読めません👼が、@ NODE_ARRAY (line: 1, code_range: (1,9)-(1,40)) って書いてありますね!

❯ ruby -e "a_code = 'a', b_code = 'b', c_code = 'c'" --dump=parsetree
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################
# @ NODE_SCOPE (line: 1, code_range: (1,0)-(1,40))
# +- nd_tbl: :a_code,:b_code,:c_code
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_PRELUDE (line: 1, code_range: (1,0)-(1,40))
#     +- nd_head:
#     |   (null node)
#     +- nd_body:
#     |   @ NODE_DASGN_CURR (line: 1, code_range: (1,0)-(1,40))
#     |   +- nd_vid: :a_code
#     |   +- nd_value:
#     |       @ NODE_ARRAY (line: 1, code_range: (1,9)-(1,40))
#     |       +- nd_alen: 3
#     |       +- nd_head:
#     |       |   @ NODE_STR (line: 1, code_range: (1,9)-(1,12))
#     |       |   +- nd_lit: "a"
#     |       +- nd_head:
#     |       |   @ NODE_DASGN_CURR (line: 1, code_range: (1,14)-(1,26))
#     |       |   +- nd_vid: :b_code
#     |       |   +- nd_value:
#     |       |       @ NODE_STR (line: 1, code_range: (1,23)-(1,26))
#     |       |       +- nd_lit: "b"
#     |       +- nd_head:
#     |       |   @ NODE_DASGN_CURR (line: 1, code_range: (1,28)-(1,40))
#     |       |   +- nd_vid: :c_code
#     |       |   +- nd_value:
#     |       |       @ NODE_STR (line: 1, code_range: (1,37)-(1,40))
#     |       |       +- nd_lit: "c"
#     |       +- nd_next:
#     |           (null node)
#     +- nd_compile_option:
#         +- coverage_enabled: false

上記は読めなかったので、rprという抽象構文木を画像にしてくれるgemを使ってみました。

hoge.rb(ファイル名は任意)に解析したいコードを書いて、同じディレクトリ上で以下を実行します。

sudo gem install rpr
rpr hoge.rb -f dot | dot -Tpng -oast.png
open ast.png


これなら読みやすいですね!ちゃんとarrayとして解釈してることもわかりました☺️
f:id:kattyan53:20210425143654p:plain

Rubyの多重代入は奥が深いこと、抽象構文木についても触れることができた良い機会になりました。以上です!