💎
ruby の rbs と debugger について2023

2023年12月19日
RBSRuby

ruby の rbs と debugger について調べてみました

この記事は matsuri technologies 株式会社 Advent Calendar 2023 の 8 日目の記事です。

# モチベーション

最近 m2m-systems というプロダクトを触っており、Ruby(rails)を書いています。 Ruby は型がないのですが、chatgpt などに書かせたり移植するときなどに型があると便利だなと思う場面が何度かありました。(バグも埋め込みにくくなるし) Ruby3.0 に RBS が導入されるのは知っていたのですが、具体的にどういうものなのかは知らなかったので調べて触ってみました。

とりあえず m2m-systems の ruby のバージョンは現在 2.7 なので ruby3.0 の rails のサンプルリポジトリを作り、そこに gem を入れていきます。

bundle init
# gemfileを編集
bundle install
bundle exec rails .
bundle exec rails s

rails new は勝手に git repo を作ってしまうので、 rm -rf .git で消します。 rails new -G でも同じ効果が得られます。

# rails

さてこれで playground の準備ができました。

Rubyの型を宣言するRBSについて調べてみた。|【㈱フクロウラボ】エンジニアブログこんにちは! バックエンドチームの久野です! 自分は、Rubyを業務で使用していますが、動的型付け言語でも静的型検査の仕組みを導入する動きが近年活発になっているので、Rubyでも型を宣言することを可能にするRBSについて調べてみました。 まずはRBSとは何かについて調べてみました。 RBSとは、Rubyの静的型付けに対応するために開発された形式的なインターフェースの一つで、RBSはRuby3.0で導入され、Rubyプログラマーによる型アノテーションの作成を容易にし、型に関する情報を共有することを可能する言語です。 上記の説明で出てきたアノテーションという単語に馴染みが無かっhttps://note.comhttps://note.comRubyの型を宣言するRBSについて調べてみた。|【㈱フクロウラボ】エンジニアブログ
Railsプロジェクトへの「頑張らない型導入」のすすめ - メドピア開発者ブログこんにちは。サーバーサイドエンジニアの三村(@t_mimura)です。 主に保険薬局と患者さまを繋ぐ「かかりつけ薬局」化支援アプリ kakariのサーバーサイド開発(Ruby on Rails)を担当しています。 突然ですが! この度kakariプロジェクトは「型導入」をしました! kakariのRailsリポジトリに型導入PRがマージされた様子 皆さんのプロジェクトは「型導入」していますか? 「型導入」しているRailsプロジェクトはまだ少ないのではないでしょうか なぜ型導入しないのか 型を導入すると何かしらが便利になることは分かっているのに何故やらないのでしょうか(煽り気味) 「型の恩恵」…https://tech.medpeer.co.jphttps://tech.medpeer.co.jpRailsプロジェクトへの「頑張らない型導入」のすすめ - メドピア開発者ブログ

ruby において型(rbs)の導入には 5 つの方法があります。

  • gem_rbs_collection や各 gem に登録されている型を取得する
  • pocke/rbs_rails などによる型の自動生成
  • typeprof による型の自動生成
  • rbs prototype による型のプロトタイプの自動生成
  • 自分で型を書く

型を導入したら当たり前だが型を活用する必要があります。

  • steeep
  • sorbet
  • rbspy
  • katakataIrb
  • コーディング時の型強化

rbs は gem として導入する必要があるので Gemfile に書いてとりあえず導入していきます。

gem 'rbs', require: false
bundle install

これで rbs コマンドが使えるようになります。

rbs collection init

rbs_collection.yaml というファイルが生成されます。 rbs_collection.yaml はどこから型情報を持ってくるかなどが定義されているみたい。

この時点で型を補完した rb ファイルを作って ruby コマンドで実行すると、壊れました。

def add(a: Integer, b: Integer) -> Integer
    return a + b
end

puts add(1, 2)
# syntax errorが出る

rbs ファイルはじゃあ何という話ですが、特に何もないです。

RBS はそれ単体で何かをするものではなく *2 、Ruby 3 の型情報を扱うツールが共通で使いたくなるものを集めた gem になっています。この gem は Ruby 3 に同梱されます。しかし基本的には型解析ツール向けの gem であり、普通の Ruby プログラマは RBS 言語を読み書きすることはあっても、RBS gem を直接使うことはあまりないと思います。

python のようにビルドイン関数やビルドインライブラリには型アノテーションがなくて、完全に別ファイルに切り出すみたいです。

イメージとしては c のヘッダファイル(.h)と実装(.c)のようなものが近いと思いました。

正直型情報だけ分けるのは python やっている人間からするとなんかキモくてあまり慣れないですね。 既存のファイルに手を入れずに済むのはメリットかなと思いました。

次に typeprof を導入してみました。

gem 'typeprof', require: false
bundle install

これで typeprof コマンドが使えるようになります。

Rubyの型を宣言するRBSについて調べてみた。|【㈱フクロウラボ】エンジニアブログこんにちは! バックエンドチームの久野です! 自分は、Rubyを業務で使用していますが、動的型付け言語でも静的型検査の仕組みを導入する動きが近年活発になっているので、Rubyでも型を宣言することを可能にするRBSについて調べてみました。 まずはRBSとは何かについて調べてみました。 RBSとは、Rubyの静的型付けに対応するために開発された形式的なインターフェースの一つで、RBSはRuby3.0で導入され、Rubyプログラマーによる型アノテーションの作成を容易にし、型に関する情報を共有することを可能する言語です。 上記の説明で出てきたアノテーションという単語に馴染みが無かっhttps://note.comhttps://note.comRubyの型を宣言するRBSについて調べてみた。|【㈱フクロウラボ】エンジニアブログ
ここで紹介されているようなコード持ってきて、これに typeprof をかけてみます。


(base) root ➜ /workspaces/vscode-devcontainer-test/rails-with-rbs/rbssample (feat/rails-with-rbs2) $ typeprof typeprofTest.rb

# TypeProf 0.21.8

# Classes

class Information
@animal: untyped
@height: untyped

def initialize: (animal: untyped, height: untyped) -> void
end

コンソールにコードが出力されました。 クラスだけじゃ分かりにくいかなと思って sample.rb の関数にも型情報を消して typeprof をかけてみたところ、定義だけかと思ったらこちらもクラスが作られました。 RBS ファイルの実態は元のコードが関数でもクラスでもクラスとして登録されるようです。


# TypeProf 0.21.8

# Classes

class Object
private
def add: (Integer a, Integer b) -> Integer
end

他の使い方としては typeprof には引数を 2 つ渡して-v を付けることでエラーが出る可能性のあるところを示せます。


(base) root ➜ /workspaces/vscode-devcontainer-test/rails-with-rbs/rbssample (feat/rails-with-rbs2) $ typeprof sample.rb sample.rbs -v

# TypeProf 0.21.8

# Errors

sample.rb:5: [error] failed to resolve overload: Object#puts
sample.rb:2: [error] failed to resolve overload: Integer#+

# Classes

typeprof だけで簡単に型チェックができるのは便利ですね。CI とかにチェッカーとして置いておくと良さそうだと思いました。

-o で rbs ファイルとして出力出来ます。

typeprof sample.rb -o sample.rbs

出力されたファイルは steep check(や typeprof の vscode extension) で使えます。これ単体であっても生の ruby には型構造が無いので何も使えなさそう()

次に steep を導入してみます。typeprof で作ったファイルを steep や sorbet で検査するという流れですね。

gem 'steep', require: false
bundle install

steep は steep init しないと使えません。 steep init すると Steepfile が生成されます。 慣習に倣って sig ディレクトリに rbs を格納してそこから読み取れるように steepfile を編集します。(コメントアウトを外す)

ここまで来て初めて steep check を走らせることが出来ます。


(base) root ➜ /workspaces/vscode-devcontainer-test/rails-with-rbs/rbssample (feat/rails-with-rbs2) $ steep check

# Type checking files:

...................................................................................

No type error detected. 🍵

mocha の test みたいなのが出力が出てきました。 sample.rb にエラーになるようにガチャガチャやってみたが関数などには効果がない?引数を変えたり新たなメソッドを足しても特に steep check はエラーになりませんでした。

次に sorbet を導入してみます。


gem 'sorbet', require: false

sorbet は ruby の中に型アノテーションとして書けるらしい。

srb init しようとしたら deprecated になっていた・・・マジ?

(base) root ➜ /workspaces/vscode-devcontainer-test/rails-with-rbs/rbssample (feat/rails-with-rbs2) $ bundle exec srb init
The srb rbi command is in maintenance mode, please use Tapioca instead.

また、sorbet を mac の m2 上で動かそうとしたところ、対応していない的なエラーが表示されました。

srb からはいったん離れて vscode 及び rubymine で型チェック出来る方法について調べてみます。

とりあえず typeprof の公式 wiki にしたがって rbswiki を clone して bundle install して rbs install をしてみました。rbs には rbs init のほかにも rbs install もあるようです。

rbs collection init は rbs_collection.yaml を作るコマンドで、rbs collection install は rbs_collection.yaml に書かれた情報を元に rbs ファイルをダウンロードするコマンドです。

コマンドは色々あるようです。

この状態で typeprof extension を入れてチュートリアル通り動くかと思ったけど動かず。


(base) root ➜ /tmp/rbswiki (main) $ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]
(base) root ➜ /tmp/rbswiki (main) $ bundle exec typeprof --version
typeprof 0.20.2
(base) root ➜ /tmp/rbswiki (main) $

上手くいかず。log を見ると port がエラーを履いているので LSP が devcontainer 上だと上手く通信出来てないかも。

local の windows 上で再度実行してみると上手く vscode 上で表示してくれました。

image (別のマシンでコードだけコピペで動かしているため画像だと untyped になっていますが、このように関数の上に推論結果を表示してくれます。rbswiki には型情報があるためそれに従って表示してくれるはずです。)

一度 rbs コマンドと typedef の拡張さえ入れてしまえば、適当なディレクトリ(ここでは/tmp/testrb)などを作ってそこに sample.rb などを生やしても(rbs gem や typeprof gem などをディレクトリごとに bundle インストールしなくても)雑に推論してくれます。ゆるく型情報を見たい時などに便利ですね。

image

特に何もしていなくても型情報が推論されるのはかなり体験がいいです。

最初は特に何も情報を与えないと untyped になっています。(自分はこれだけでも他の関数から参照する時に引数の数がわかるので十分ありがたいです。)

image

(後の比較のためにこの状態で typeprof sample.rb -o sample.rbsを呼んで rbs を生やしておきます。)

この状態で add を呼ぶと勝手に推測して型を表示してくれました。

image

色々と型を変えてみます。

image なんと add(1,2)だと number ですが add("1",2)だと第一引数が表示される型が勝手に string に変わります。リアルタイムに型が変わっています。 キモいですね。

この状態で sample.rbs を見てみると全て untyped になっていました。どうやらここの値は sample.rbs を見ているのではなく、標準の typeprof の推測によるもののようです。

image

typeprof で作られた rbs ももちろん String になっているんですが add の引数を Int に変えても typeprof -v で合わせてみるとエラーが表示されました。

❯ typeprof sample.rb sig/sample.rbs -v
Importing RBS::AST::Members::ClassInstanceVariable is not supported yet
Importing RBS::AST::Members::ClassInstanceVariable is not supported yet
Importing RBS::AST::Members::ClassInstanceVariable is not supported yet
Importing RBS::AST::Members::ClassInstanceVariable is not supported yet
Importing RBS::AST::Members::ClassInstanceVariable is not supported yet
Importing RBS::AST::Members::ClassInstanceVariable is not supported yet
Importing RBS::AST::Members::ClassInstanceVariable is not supported yet
Importing RBS::AST::Members::ClassInstanceVariable is not supported yet
# TypeProf 0.21.8

# Errors
sample.rb:5: [error] failed to resolve overload: Object#add
sample.rb:6: [error] failed to resolve overload: Object#add
sample.rb:2: [error] failed to resolve overload: Integer#+

# Classes

また、ディレクトリに typeprof.rbs というファイルを生やしておくと、それに従って型情報などを矯正してくれます。 トップディレクトリにある typeprof.rbs からしか型情報は読み取らないようです。逆にこれが置かれていないと自動で推論するような挙動になります。個人的には好みですが挙動を理解してないと罠にハマりそうですね。 image

明示的な型情報以外表示させたく無い場合、ruby Solargraph という拡張機能もあり、こちらは rbs のファイルなどをファイルを明示的に yaml ファイルなどに書いておくことができるようです。(試してないので紹介程度にとどめておきます)

# debugger について

RBS を入れる前に、デバッガーが動いてなかったため(なにぶん古くからあるリポジトリなので)、デバッガーについて調べて導入するプルリクも作ったのですが長くなりそうなのでまた今度にします。

  • ruby-debug-ide
  • debase
  • debug(gem 名)・・・rdbg が使えるようになる
  • ruby -r debug app.rb(lib/debug.rb)
  • byebug
  • pry-debug

この辺を参考にして、byebug や debug.gem、debase&ruby-debug-ide などを試して ruby2.7 だと debase&ruby-debug-ide が上手く動作してブレークポイントが引っかかり、 rubymine と vscode で動作確認ができました:clap:

# 感想

LSP さえ起動できれば typeprof は使えそうなので良かったです。(まぁ推測なので型情報信頼できないのとそれによる補完とかは使えないですが。) デバッガについても同じぐらい語りたいのですが、長くなりそうなのでまた今度にします。(2 回目)

私は静的型付け言語があまり好きではなく、TypeScript ぐらいのゆるさの型が丁度いいと思っているのですが、流石に型が全くないというのはやはり不便だなと感じました。いつか m2m-systems も ruby3 系まで上げて rbs を導入してみたいですね。

# we are hiring

ruby についてたくさん語りましたが社内のプロダクトのバックエンドの大半は GO や Rust などの静的型付け言語でできています。(front は React+TypeScript がメインです。) カジュアル面談もやっていますので go や rust などの静的型付け言語が好きという方は是非ご応募ください。ご飯代が出るので社内の人は喜ぶんじゃないでしょうか、きっと。