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

Shibuya.rb-2023-04-27-igaiga

 Shibuya.rb-2023-04-27-igaiga

RuboSensei
Real-time Analysis Improved Learning Experience
五十嵐邦明 / igaiga, Shibuya.rb 2023/04/27

Kuniaki IGARASHI

April 27, 2023
Tweet

More Decks by Kuniaki IGARASHI

Other Decks in Technology

Transcript

  1. RuboSensei Real-time Analysis Improved Learning Experience 五十嵐邦明 / igaiga, Shibuya.rb

    2023/04/27 今日のまとめ Rubyを教えてくれるRuboSenseiをつくっている TypeProfを型推論ライブラリとしてつかいたい RubyKaigi 著者スタンプラリー "Rubyist Book Author" のお知らせ 新刊「RubyとRailsの学習ガイド2023」本日発売 月1〜4日でお仕事募集中
  2. RuboSensei アーキテクチャ RuboCop カスタムCopとして実装 RuboCopエコシステムをつかうとVSCode上でリアルタイムにアドバイス可能 RuboCop VSCode Ruby Light plugin

    https://marketplace.visualstudio.com/items? itemName=r7kamura.vscode-ruby-light VSCode上でリアルタイムにRuboCop結果を表示 静的解析 構文解析 Parser gem (RuboCop標準) 静的解析 型推論 TypeProf
  3. RuboCop カスタムCopとの性質の違い 対象者の習熟度の違い RuboCop 対象者: Rubyに習熟している 学習者に対してはレベルを超えたアドバイスになることも RubyKaigi2022 "The Better

    RuboCop World to enjoy Ruby" でも問題提起 RuboSensei 対象者: Rubyにはまだ詳しくない学習者 学習者が理解しやすいアドバイスを出力 修正点以外でコードの理解を助けるアドバイスも出力 CopのON/OFF RuboCop: コード品質向上を目的として永続的に有効化して運用 RuboSensei: 学んで理解したアドバイスは無効化したい
  4. RuboSenseiで実装したカスタムCop foo.bar{|x| x.baz } は foo.bar(&:baz) で置き換えできるかも foo.bar(&:baz) は foo.bar{|x|

    x.baz } と同じ この each メソッドは map メソッドで置き換えできるかも elsif は case で書き換え可能 unless else はやめて if で置き換えよう
  5. RuboCop Copの基本形 例: ["a","b"].map{|x| x.upcase} ["a","b"].map(&:upcase) 分析対象: method1{ |x| x.method2

    } アドバイス: method1(&:method2) で置換できる module RuboCop::Cop::Lecture # good & bad comment ( 省略) class PreferSymbolToProc < Base MSG = " このブロックはmethod(&:method) で置き換えられるかもしれません。" def on_block(node) # パース結果AST にブロックなnode が出たとき if ... # AST を判定する add_offense(node) # RuboCop にメッセージを出させる end end end end
  6. AST # 対象コード: array.map{|x| x.upcase} def on_block(node) # node #=>

    # s(:block, # s(:send, # s(:send, nil, :array), :map), # ここは判定に不要 # s(:args, # s(:arg, :x)), # ここは判定に不要 # s(:send, # ブロック中の式が1 つだけ判定 # s(:lvar, :x), :upcase)) # 引数なし判定 「ブロック中に1メソッドしか書いていない」 ブロック中に2メソッド以上書かれているときにはASTにbeginノードが入る beginノードがないことで判定 メソッドであることは node.send_type? で判定 引数有無は node.arguments? で判定
  7. 判定コード # 対象コード: array.map{|x| x.upcase} def on_block(node) # node #=>

    # s(:block, # s(:send, # s(:send, nil, :array), :map), # ここは判定に不要 # s(:args, # s(:arg, :x)), # ここは判定に不要 # s(:send, # ブロック中の式が1 つだけのとき、:begin ノードが親に入らない # s(:lvar, :x), :upcase)) # 引数なし target_nodes = node.child_nodes - [node.send_node] # map メソッドnode は除く send_type_nodes = target_nodes.select(&:send_type?) # :begin node が入らず、直下にあること if send_type_nodes.count == 1 && !send_type_nodes.last.arguments? # 引数なし add_offense(node) # アドバイス可能なnode であることを指示 end end ["a","b"].map{|x| x.upcase} ["a","b"].map(&:upcase) を表示できる
  8. ASTとTypeProf型推論を組み合わせて分析 Parserで解析したASTのノードの型情報も欲しいケース 例:「このeachメソッドはmapメソッドで置き換えできるかも」 result = [] [1,2,3].each do |x| result

    << x * 2 end #=> [2,4,6] 「レシーバresultの型がArrayであること」を知りたい 型の情報をTypeProfで推論する TypeProfへRubyコード片を渡して分析したい
  9. TypeProfへRubyオブジェクトを入出力して分析するコード(入力編) ## レシーバの型がArray であるかどうかをTypeProf で調査 rb_text = node.parent.source # レシーバの定義元を含んだコード片を取得

    receiver_variable_name = node.child_nodes.last.receiver.node_parts.first.to_s rb_text += "\n" + "p #{receiver_variable_name}" # TypeProf 分析のため p レシーバ を追加 target_line_number = rb_text.lines.count # TypeProf 出力結果の解析に行番号が必要 rb_files = [["target.rb", rb_text]] # 入力 rbs_files = [] # RBS はつかわない output = StringIO.new(String.new("")) # 出力 String.new("") = mutable String object options = { show_errors: true } config = TypeProf::ConfigData.new(rb_files: rb_files, rbs_files: rbs_files, output: output, max_sec: 5, options: options) TypeProf.analyze(config) # 分析実行 RuboCop Copとしての全コード https://github.com/igaiga/rubocop- sensei/blob/v0.1.4/lib/rubocop/cop/lecture/prefer_map.rb
  10. TypeProfへRubyオブジェクトを入出力して分析するコード(出力編) #... TypeProf.analyze(config) # 分析 #=> "# TypeProf 0.21.4\n\n# Revealed

    types\n# target.rb:6 #=> Array[Integer]\n\n# Classes\n" expected_type = output.string.match(/target.rb:#{target_line_number}\s*#=>\s*(.+)$/).captures.first #=> Array[SomeClass] or untyped or somethings if expected_type.match(/(.+)\[.+\]/).captures.first == "Array" add_offense(node) # アドバイス対象ノードとしてRuboCop へ追加 end TypeProfの分析結果outputはStringIOオブジェクト ターミナル出力文字列を得る # TypeProf 0.21.4\n\n... # target.rb:6 #=> Array[Integer]\n\n... 正規表現をつかって該当部分の情報を得る ASTと型情報とを結合するために入力したコードの行番号をつかう 1行に複数式があるケースなどは誤分析になるかも TypeProfで型がArrayであることがわかった! TypeProfの型推論すごい
  11. TypeProfでこんな機能が欲しいの提言 今回はRBS出力部分と型推論部分を別々につかいたいユースケース 入出力をRubyオブジェクトで渡せる型推論APIがあると便利 入力にRubyコードのStringオブジェクトを直接渡したい 出力をHashや分析結果クラスのオブジェクトなど扱いやすいオブジェクトで得たい 構文解析結果のASTから、各ノードの型情報が得られるととても便利 型情報を含んだASTほしい! RubyKaigi 2023 mameさん

    "Revisiting TypeProf - IDE support as a primary feature" https://rubykaigi.org/2023/presentations/mametter.html Rubyコードの型推論を第一機能、Language ServerによるIDEサポートを第二機能 として提供してきた 今年はこれを逆にして、IDEを第一のターゲットにしようとしています 本講演では、TypeProfの新しい設計とそのプロトタイプを紹介する予定
  12. GitHub Copilot chat vs RuboSensei の現在 GitHub Copilot chat アドバイスの正しさを判断する必要がある

    なんでも聞いたら教えてくれる アドバイスに対して深掘りした再質問もできる RuboSensei 信用に足るアドバイスを出せる 教えてくれる内容を開発者が実装する必要がある
  13. GitHub Copilot chat vs RuboSensei の未来 GitHub Copilot chat が正しいアドバイスを常にしてくれる未来

    「正しいアドバイス」とは? 正しい = 学習者の現在のコンテキストに最適 技術的な正しさ + 学習ステージのマッチ + 目的と方向のマッチ GitHub Copilot chatは唯一神がコンテキストを動的に推測する RuboSensei は多くのコンテキストに対応できる最大公約数アドバイスを静的に実装 RuboSensei は実装者(教師)によって教えることの多様性があることが強み? ChatGPT API をつかった対話型支援システムもつくってみたい
  14. Rubyist Book Author RubyKaigi2023会場で著者訳者を探すスタンプラリー 探索先著者訳者と新刊: 鳥井 雪 「ユウと魔法のプログラミング・ノート」 江森 真由美,

    やだ けいこ, 小林 智恵 「はじめてつくるWebアプリケーション 〜Ruby on Railsでプログラミングへ の第一歩を踏み出そう」 Jeremy Evans(著)、角谷 信太郎(訳) 「研鑽Rubyプログラミング ― 実践的なコードのための原則とトレードオフ」 五十嵐 邦明 「RubyとRailsの学習ガイド2023」 【募集】著書訳書をお持ちでスタンプラリー探索対象で参加してくださる方!