Upgrade to Pro — share decks privately, control downloads, hide ads and more …

自作マクロと型生成

Avatar for a_fujisaki a_fujisaki
November 13, 2025

 自作マクロと型生成

Avatar for a_fujisaki

a_fujisaki

November 13, 2025
Tweet

More Decks by a_fujisaki

Other Decks in Programming

Transcript

  1. 自己紹介 ❏ 藤崎 亮人 (a_fujisaki) ❏ @aki19035vc ❏ イタンジ株式会社 ❏

    不動産会社向けのSaaSを提供 ❏ バックエンドエンジニア ❏ ほぼRails・たまにScala ❏ 物件システムのエンジニアリングマネージャー ❏ Rails × 型 の話が好き ❏ 学生の頃(2016年くらい)からEmacs使ってます
  2. 今日話したい事 ❏ Software Design 2025年9月号で寄稿した内容の延長 ❏ 自作マクロと型生成の難しさ ❏ 文字数が厳しくて書ききれなかったので供養 ❏

    注意事項 ❏ 以下のgemを使用したRubyコードが出てきますが、解説しないです ❏ rbs・steep・rbs-inline・prism
  3. Software Design 2025年9月号 ❏ ご縁があり、寄稿させていただきました ❏ Ruby×静的型付け戦略【5】現場における型付けRubyの実践 ❏ https://gihyo.jp/magazine/SD/archive/2025/202509 ❏

    8ページ、約11,000文字 ❏ 内容 ❏ イタンジにおける型の取り組み ❏ 現場で出会う問題と解決策 ❏ nilガードと!メソッドによる明示的な型の保証 ❏ 自作マクロと型付け戦略
  4. nilガードと!メソッドによる明示的な型の保証 class UserName attr_reader :last_name #: String attr_reader :first_name #:

    String end class User attr_reader :name #: UserName? # nilの可能性を排除するメソッドを定義 # #: () -> UserName def name! name || raise end end user = User.new user.name.last_name #=> nilの可能性があるので型エラー if user.name.nil? # ... else user.name.last_name #=> nilの可能性があるので型エラー user.name!.last_name #=> 型チェックが通る end
  5. def_nil_bangマクロ class UserName attr_reader :last_name #: String attr_reader :first_name #:

    String end class User extend DefNilBang attr_reader :name #: UserName? # @rbs! # def name!: () -> UserName def_nil_bang :name end module DefNilBang def def_nil_bang(*method_names) method_names.each do |name| define_method(:"#{name}!") do value = public_send(name) raise if value.nil? value end end end end
  6. マクロで生えるメソッドの型を自動で生成したい class UserName attr_reader :last_name #: String attr_reader :first_name #:

    String end class User extend DefNilBang attr_reader :name #: UserName? def_nil_bang :name end class UserName attr_reader last_name: String attr_reader first_name: String end class User extend DefNilBang attr_reader name: UserName? def name!: () -> UserName end rbs生成
  7. class DefNilBangVisitor < Prism::Visitor attr_reader :results def initialize super() @class_name

    = nil @results = [] end def visit_class_node(node) previous_class_name = @class_name @class_name = node.name visit(node.body) if node.body @class_name = previous_class_name end def visit_call_node(node) return if @class_name.nil? || node.name != :def_nil_bang node.arguments.child_nodes.each do |child_node| @results << { class_name: @class_name, method_name: child_node.value } end end end def generate_def_nil_bangs_rbs(rbs_env, ruby_path, io) result = Prism.parse(File.read(ruby_path)) return unless result.success? visitor = DefNilBangVisitor.new visitor.visit(result.value) return if visitor.results.empty? sigs = visitor.results.filter_map do |result| type_name = RBS::TypeName.new( namespace: RBS::Namespace.parse('::'), name: result[:class_name] ) rbs_env.class_decls[type_name].primary.decl.members.filter_map do |member| if member.is_a?(RBS::AST::Members::AttrReader) && member.type.is_a?(RBS::Types::Optional) && member.name == result[:method_name].to_sym <<~"RBS" class #{type_name} def #{result[:method_name]}!: () -> #{member.type.type} end RBS end end end io.puts sigs.join("\n") end def_nil_bangマクロ 型の生成処理
  8. 型の自動生成は意外と難しい ❏ サンプルプログラムはattr_readerで定義されたオプショナルな型のみを考慮 ❏ 実際はもっと複雑 ❏ ユニオン型の考慮 ❏ メソッド ❏

    モジュールをインクルードした後のメソッド ❏ rbsのジェネリクス型やインターフェース ❏ 結論 ❏ rbs-inlineを使って型を直接埋め込むほうがコストパが良い