Ruby

RubyのAST入門 #omotesandorb

表参道.rb #34
https://omotesandorb.connpass.com/event/86444/

の発表資料。


自己紹介

名前: sinsoku
会社: 株式会社DMM.comラボ(🔞🙅)
副業: 株式会社grooves
github: sinsoku
twitter: @sinsoku_listy


話すこと

  1. ASTの概要
  2. 実務で役立つAST
  3. ASTの限界
  4. そして型推論の夢を見る...

AST(抽象構文木)の概要


ASTを知っている人?✋


ASTの概要

Rubyはコードを下記の順で解析され、実行されます。

  1. 字句解析
  2. 構文解析
  3. YARV(Yet Another Ruby VM) 上で実行
    • コードをiseqにして動かす
    • iseq(InstructionSequence)

字句解析

Rubyのコードを単語に分ける。

Ripper.lex("a = 1 + 1").each { |t| p t }
# [[1, 0], :on_ident, "a", #<Ripper::Lexer::State: EXPR_CMDARG>]
# [[1, 1], :on_sp, " ", #<Ripper::Lexer::State: EXPR_CMDARG>]
# [[1, 2], :on_op, "=", #<Ripper::Lexer::State: EXPR_BEG>]
# [[1, 3], :on_sp, " ", #<Ripper::Lexer::State: EXPR_BEG>]
# [[1, 4], :on_int, "1", #<Ripper::Lexer::State: EXPR_END>]
# [[1, 5], :on_sp, " ", #<Ripper::Lexer::State: EXPR_END>]
# [[1, 6], :on_op, "+", #<Ripper::Lexer::State: EXPR_BEG>]
# [[1, 7], :on_sp, " ", #<Ripper::Lexer::State: EXPR_BEG>]
# [[1, 8], :on_int, "1", #<Ripper::Lexer::State: EXPR_END>]

構文解析

単語からRuby構文を表現するAST(抽象構文木)を作る。

Ripper.sexp("a = 1 + 1")
# [:program,
#  [[:assign,
#    [:var_field, [:@ident, "a", [1, 0]]],
#    [:binary, [:@int, "1", [1, 4]], :+, [:@int, "1", [1, 8]]]]]]

YARV上で実行

ASTをiseqという命令列に変換し、YARV上で実行する。
詳細を知りたい人は下記の本を読むと良いです。


実務で役立つAST


RuboCop の拡張がオススメ!


💀404 NotFound


資料が間に合わなかった。。。


ASTの限界


👮 RuboCop DynamicFindBy

class User < ActiveRecord::Base
end

User.find_by_name("foo") # <= 1 offence

👮 false positive

RuboCop さんは下記のコードを誤検知します。

class Foo
  def self.find_by_name(name); end
end

Foo.find_by_name("bar")

なぜ誤検知するのか?

RuboCop はメソッド名のみをチェックしていて、
ActiveRecord::Base の子孫かどうかはチェックしていない。

Rubyには型がないため、どうしようも無い...。


そして型推論の夢を見る


リテラルの型は分かる

num = 1
#=> Integer型
array = []
#=> Array型

newの戻り値の型も分かる

class User
end

user = User.new
#=> User型

newの戻り値の型も分かる

Ripper.sexp("user = User.new")
# [:program,
#  [[:assign,
#    [:var_field, [:@ident, "user", [1, 0]]],
#    [:call,
#     [:var_ref, [:@const, "User", [1, 7]]],
#     [:@period, ".", [1, 11]],
#     [:@ident, "new", [1, 12]]]]]]

(Railsの)schem.rbも使えそう

create_table "users" do |t|
  t.string "email", default: "", null: false
  t.integer "age", null: false
end

ASTで推論できるかも?


上書きされるnew

class User
  def self.new
    Admin.new
  end
end

user = User.new
#=> Admin型

引数で変わる型

def pick(*column_names)
  limit(1).pluck(*column_names).first
end

User.pick(:name)
#=> "David Heinemeier Hansson"
User.pick(:name, :admin)
#=> ["David Heinemeier Hansson", true]

世知辛いのじゃ...

  • Ruby の世界は厳しい
  • ただ、7割くらい推論できる気がする

まとめ

  • ASTは意外と簡単なのでみんな触ろう!
  • 型に興味ある人はぜひ話しましょう!

ご清聴ありがとうございました。