Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

配属1ヶ月のインターンが勝手にRBS::Inlineを導入して怒られた

ogp

この記事は、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を混ぜました。
そうしたらレビューでこんなコメントが・・・

review-comment

はい、当然ですね。
レビュアーの方には、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-inlinesteepをインストールする必要があります。

$ bundle add rbs-inline steep

2. .rbファイルにRBS::Inlineコメントを記述する。

今回はサンプルとしてperson.rbmain.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をインストールします。
steep-error
このようにエディタ上で型エラーが表示されるようになります。便利!

.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では、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com