この記事は、Techouse Advent Calendar 2024 7日目です。
昨日は daiki_fujioka さんによる OIDCの仕組みを完全理解して、SaaSプロダクトに2FA機能を実装しました でした。
7日目は、澤井(aaaa777) が担当します。
今年8月から株式会社Techouseにてインターンで働いている澤井(aaaa777)です。
みなさんはRBS::Inlineをご存知でしょうか。
RBS(Rubyに型システムを導入するインターフェース言語)をコメントから生成出来るようにしたものです。
先日参加したRubykaigi 2024 follow upというイベントでRBS::Inlineを知り、これはいいものだと早速私は会社のPRに勝手にRBS::Inlineを混ぜました。
そうしたらレビューでこんなコメントが・・・
はい、当然ですね。
レビュアーの方には、RBS::Inline自体は気になっているが検討するためにまずRBS::Inlineを導入するために必要な準備をしてみてはどうかと言われました。
確かにYARDがあるのになぜRBS::Inlineを導入する必要があるのか、と思う方もいるでしょう。
この記事ではRBS::Inlineを導入するために必要な準備と導入方法について調べた情報をまとめ、みなさんのRBS::Inlineを導入するモチベーションを高めることを目指します。
RBS::Inlineの使い方
では、具体的にRBS::Inlineとsteepを利用した型を検査する手順を説明します。
1. 必要なライブラリをインストールする。
2. .rb
ファイルにRBS::Inlineコメントを記述する。
3. rbs-inline
コマンドでsig/generated
下に.rbs
ファイルを生成する。
4. steep init
コマンドでSteepfile
を作成する。
5. steep check
コマンドで型を検査する。
実際にサンプルコードを作成しつつ、手順を説明していきます。
1. 必要なライブラリをインストールする
RBS::Inlineを利用するためにはrbs-inline
とsteep
をインストールする必要があります。
$ bundle add rbs-inline steep
2. .rb
ファイルにRBS::Inlineコメントを記述する。
今回はサンプルとしてperson.rb
とmain.rb
を作成します。
# lib/person.rb # rbs_inline: enabled class Person # @dynamic name attr_reader :name #: String # @dynamic addresses attr_reader :addresses #: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs return: void def initialize(name:, addresses:) @name = name @addresses = addresses end def to_s #: String "#{name} (#{addresses.join(', ')})" end # @rbs &block: (String) -> void def each_address(&block) #: void addresses.each(&block) end end
# lib/main.rb require_relative 'person' person = Person.new person.name = 'Alice' puts person.name
ちなみに現時点でmain.rb
を実行するとエラーになるポイントが2箇所ありますが、分かりますか?
3. rbs-inline
コマンドでsig/generated
下に.rbs
ファイルを生成する
.rbs
ファイルの保存先はsig/
下のサブディレクトリという慣例です。
rbs-inline
コマンドで生成された.rbs
ファイルはデフォルトでsig/generated/
下に配置します。
$ bundle exec rbs-inline --output lib # --outputを外すと標準出力に出力される
成功するとsig/generated/person.rbs
が生成されます。
4. steep init
コマンドでSteepfile
を作成する
型検査ツールSteepではSteepfile
というファイルで型検査の設定します。
steep init
コマンドでSteepfile
を作成し、内容を以下のように編集します。
$ bundle exec steep init
# Steepfile target :lib do check "lib" signature "sig/generated" end
5. steep check
コマンドで型を検査する
型を検査するにはsteep check
コマンドを実行します。
$ bundle exec steep check # Type checking files: .....................................F................................................. lib/main.rb:3:16: [error] More keyword arguments are required: name, addresses │ Diagnostic ID: Ruby::InsufficientKeywordArguments │ └ person = Person.new ~~~ lib/main.rb:4:12: [error] Type `::Person` does not have method `name=` │ Diagnostic ID: Ruby::NoMethod │ └ person.name = 'Alice' ~ Detected 2 problems from 1 file
手順通りに進めると、main.rb
のエラーが表示されるはずです。
型情報を基に#initialize
時のキーワード引数が足りないことと、#name=
メソッドが存在しないことを指摘されました。
Rubyで静的に型検査が出来るの凄くないですか?
僕は感動しちゃいました。
VSCodeでの利用
せっかく型システムを導入したので、エディタでの補完を有効にしておきましょう。
VSCodeの場合Steepをインストールします。
このようにエディタ上で型エラーが表示されるようになります。便利!
.rbsファイルを自動で再構築する
ファイルを変更したら.rbs
ファイルを再度生成する必要がありますが、人間はこういう処理をよく忘れるため、便利な自動再実行コマンドを紹介します。
以下のコマンドを実行すると、lib
ディレクトリ内のファイルが変更されるたびにrbs-inline
コマンドを実行して.rbs
ファイルを再生成することができます。
$ fswatch -0 lib | xargs -0 -n1 bundle exec rbs-inline --output
コマンドの説明
- fswatch: ファイルの変更を検知して標準出力に出力するコマンドです。
OSが提供するファイル変更通知機能を利用して、指定したディレクトリ内でファイルが変更されたときにそのファイル名を標準出力に出力します。 - xargs: 標準入力から受け取った文字列をコマンドライン引数として実行するコマンドです。
この2つのコマンドを組み合わせることにより、手動でファイルを監視する手間を省くことができます。
なおRBS Helperという拡張機能にはこのコマンドと同等の再実行機能があるようです。
まだ試していませんがrubocop-rbs_inlineというRuboCop用のRBS::Inline拡張機能も開発されていて、こちらも自動化に役立ちそうです。
YARDからRBS::Inlineへの移行するメリット
ここまでRBS::Inlineの導入方法と効果について説明しましたが、YARDからRBS::Inlineに移行するメリットが伝わったでしょうか。
RBS::Inlineを利用するメリットは型を利用したエコシステムの上に乗れることです。
今回示したように型エラー検出の仕組みやその周りのツールは既にかなりの完成度になっており、エディタ上での補完や型エラーの表示はとても便利だと感じました。
RBS::Inlineやこれらのツールを導入することで、より快適に開発できるようになるのです。
終わりに - それでもRubyで型を使いたい
そんなに型が欲しいなら他の言語で書けばいいという意見もあります。わかります。
まつもとゆきひろ氏(Rubyの生みの親)も
型を言語仕様として取り入れるつもりは今のところ無いそうですし、型が欲しいだけならRubyである必要はないですね。
では何故そんなRubyで型を導入するのか。それは既に軌道に乗ったプロダクトに秩序を持ち込むためです。
特にすでに数年が経っているプロダクトの方で、そこに秩序を持ち込むための手段として型を望んでいるところは多いのではないでしょうか。
プロダクトが小さく人も少ないうちは、型を書かない方が素早く立ち回れる場合もあるでしょう。
ただプロダクトが成長し人が増えると各々のスキルにばらつきが生じ、仕様の解釈違いがバグとデグレを生み出しはじめます。
既に示した通り、Inlineの導入でこれらのエラーは事前に検知できるようになります。
NoMethodError for nil
なんかはもう見たくないですよね。
型システムの導入でみんな幸せになりましょう?
明日のTechouse Advent Calendar 2024は 高橋 (@Kaffff) さんによる localStorage の値を Zod で安全にパースする です。
今回利用したサンプルコード
サンプルコードはこちらです。
aaaa777/test-rbs-inline
参考リンク
Ruby 3の静的解析機能のRBS、TypeProf、Steep、Sorbetの関係についてのノート - クックパッド開発者ブログ
RBS::Inline を導入してみました (2) | Webシステム開発/教育ソリューションのタイムインターメディア
最速で体験する RBS + Steep による 型チェック
soutaro/steep-vscode: VSCode extension for Steep
Ruby に型が欲しい理由 | blog.euxn.me
「自分の未来予測を信じてちょっと意地を張ってみる」 まつもとゆきひろ氏がRubyに型宣言を入れない理由 | ログミーBusiness
Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。