Slide 1

Slide 1 text

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日でお仕事募集中

Slide 2

Slide 2 text

RuboSenseiデモ https://github.com/igaiga/rubocop-sensei

Slide 3

Slide 3 text

エディタ上で 一般的に受けられる支援 コードハイライト コード補完 GitHub Copilot コード生成 Copilot chat 後述 Copilot for docs

Slide 4

Slide 4 text

RuboSensei をつくりたい コードを書いているときに機械からアドバイスを受けたい 人間に聞くよりも早く気軽に 一般的な学ぶ方法 書籍、Web、ChatGPT、動画 コードを書いているタイミングで教えて欲しい RuboSenseiで目指すところ コードを読むとき: リアルタイムに解説を表示して欲しい コードを書くとき: より良い書き方があったら教えてほしい

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

RuboCop カスタムCopとの性質の違い 対象者の習熟度の違い RuboCop 対象者: Rubyに習熟している 学習者に対してはレベルを超えたアドバイスになることも RubyKaigi2022 "The Better RuboCop World to enjoy Ruby" でも問題提起 RuboSensei 対象者: Rubyにはまだ詳しくない学習者 学習者が理解しやすいアドバイスを出力 修正点以外でコードの理解を助けるアドバイスも出力 CopのON/OFF RuboCop: コード品質向上を目的として永続的に有効化して運用 RuboSensei: 学んで理解したアドバイスは無効化したい

Slide 7

Slide 7 text

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 で置き換えよう

Slide 8

Slide 8 text

RuboCop カスタムCopの実装方法概要 Railsの練習帳 「RuboCop カスタムCopのつくり方」 https://zenn.dev/igaiga/books/rails-practice-note/viewer/rails_rubocop_custom_cop

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

作戦 ["a","b"].map{|x| x.upcase} ["a","b"].map(&:upcase) 上記の書き換えが可能な条件をASTから判定する作戦を考える 誤検知がたまに出ても良いことにして、実利を優先する指針 判定条件 ブロック中に1つの式(メソッド)しか書いていない その唯一のメソッドに引数が渡されていない アドバイスを含んだRuboCop Copとしての全コード https://github.com/igaiga/rubocop- sensei/blob/v0.1.4/lib/rubocop/cop/lecture/prefer_symbol_to_proc.rb

Slide 11

Slide 11 text

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? で判定

Slide 12

Slide 12 text

判定コード # 対象コード: 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) を表示できる

Slide 13

Slide 13 text

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コード片を渡して分析したい

Slide 14

Slide 14 text

TypeProfとは Ruby標準添付の型推論ライブラリ ターミナルでRubyコードファイルを入力にRBSファイルを出力する $ typeprof foo.rb -o foo.rbs おそらくこれが主用途 今回は入出力をファイルではなくRubyオブジェクトにしたい 謝辞: TypeProfのつかい方をmameさんにサポートいただきました もしかすると、別解としてTypeProfをLSPとしてつかえば楽なのかも?

Slide 15

Slide 15 text

TypeProfへRubyオブジェクトを入出力して分析したい 出力: StringIOオブジェクト(標準入出力とStringを変換できる) 入力: ダミーファイル名文字列と対象Rubyコード文字列の配列の配列 [["target.rb", target_ruby_code_string]] 加えて、入力するRubyコードに細工 を加える必要がある 型情報を得たい変数へ代入しているコードを加える ブロックノードの親ノードを渡す 必要に応じてさらに上部を含んでも良さそう TypeProfは対象変数の出力がないと解析対象としない 「p 変数」コードを追加して、TypeProfが解析可能な形のコードを作成

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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の型推論すごい

Slide 18

Slide 18 text

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の新しい設計とそのプロトタイプを紹介する予定

Slide 19

Slide 19 text

GitHub Copilot https://docs.github.com/ja/copilot/getting-started-with-github-copilot コード生成 Copilot chat VSCode上でチャットで聞きながらコードを書ける Waitlist に並んでつかう https://github.com/github-copilot/chat_waitlist_signup/ Copilot for docs GitHub上のドキュメントから良い感じに答えてくれる Waitlist に並んでつかう https://githubnext.com/projects/copilot-for-docs

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

GitHub Copilot chat vs RuboSensei の未来 GitHub Copilot chat が正しいアドバイスを常にしてくれる未来 「正しいアドバイス」とは? 正しい = 学習者の現在のコンテキストに最適 技術的な正しさ + 学習ステージのマッチ + 目的と方向のマッチ GitHub Copilot chatは唯一神がコンテキストを動的に推測する RuboSensei は多くのコンテキストに対応できる最大公約数アドバイスを静的に実装 RuboSensei は実装者(教師)によって教えることの多様性があることが強み? ChatGPT API をつかった対話型支援システムもつくってみたい

Slide 22

Slide 22 text

お仕事募集中!! 【募集】月1〜4日程リモートお仕事(技術顧問、育成、実装ほか)探してます! レガシー改善実装、Railsバージョンアップ実装 コード健康診断 ペアプロ屋(エンジニアや他職種(QA, CS他)の方と) パーフェクトRailsやRailsの練習帳ほか読書会 詳細: https://garnettech373.com/services

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Rubyist Book Author 開催意図: 話しかけられると著者たちが嬉しい! 著者訳者から本を直接買える!サインも もらえる!(きっと) 参加方法: 著者訳者を探してスタンプ台紙をもらう 著者訳者と話す、本を買っても良い 他の著者訳者を探してスタンプをもらう 著者訳者と話す、本を買っても良い ※ 内容は変更されることもあります

Slide 26

Slide 26 text

RubyとRailsの学習ガイド2023 https://igaigarb.booth.pm/items/4706929 BOOTHにて本日より販売開始 RubyKaigi2023特別企画 PDF版をお買い上げの方へノベルティと して物理書籍版をプレゼント RubyKaigi会場で私を探して購入履歴を ご提示ください