rbs_rails & steep で型のある Rails 開発を体験しよう

こんにちは。スタメンの河井です。
RubyKaigi Takeout 2020 が楽しみですね。

Ruby 3.0 から型定義 & 型検査ができるようになると言われていますが、今の段階でもそれに関連した gem は公開されています。
今回は型のある Rails 開発を体験してみようということで、RBS・rbs_rails・Steep の3つの gem を紹介しようと思います。

RBS とは

RBS とは、 Ruby プログラムの構造を記述するための言語です。
Ruby のソースコード(.rb ファイル)とは別にファイル(.rbs)を用意して型定義を記述していきます。

たとえば

# message.rbs

class Message
  def reply: (from: User | Bot, string: String) -> Message
end

という定義では

  • Message クラスのインスタンスメソッド reply
  • 引数は User または Bot のインスタンスと String のインスタンスの2つ
  • 返り値は Message のインスタンス

といったことを表します。

詳しい文法はこちらを参照ください。

Rails 関連メソッドの型生成

RBS の役割は型を定義することであり、チェックの機能は備えていません。
そこで、Steep という gem(後述)を使用して RBS を読み込んで型チェックをします。

あるクラスの型検査を行うためには、そのクラスが継承しているクラスについても型定義を見る必要があります。
つまり、Rails のソースコードを検査しようと思うと ActiveRecord::Base など Rails が提供しているクラスの定義が必要になってきます。
それらを自前で用意するのはかなり大変なので、rbs_rails という gem に助けてもらいましょう。

rbs_rails は以下の2つの機能を持っています。

  1. Rails が用意してくれているクラスの型定義ファイルの生成
  2. ユーザーが定義したモデルクラスの型定義の生成

rbs_rails#usage に従って Rake Task を実行します。
これによって生成される Rails の型定義ですが、たとえば ActiveRecord_Relation の型はこのようになります。 f:id:natsuokawai:20200914111258p:plain

次はモデルからの型生成です。 たとえば次のような Book モデルの定義からは

# app/models/book.rb

class Book < ApplicationRecord
  belongs_to :user
end
# db/schema.rb

ActiveRecord::Schema.define(version: 2020_08_26_150533) do
  create_table "books", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
    t.string "title"
    t.bigint "user_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_books_on_user_id"
  end
end

このような型定義が生成されます。

f:id:natsuokawai:20200914111755p:plain

f:id:natsuokawai:20200914111759p:plain

カラム名に応じて動的に定義されるメソッドや、belongs_to で定義されるメソッドの型定義が生成されています。便利…

型チェックによる型エラーの発見

ここからは Steep を使ってソースコードの型チェックを実行してみます。

設定ファイルの作成

まず初めに、Steepfile というファイルで型検査のターゲットや型定義のディレクトリを指定します。ここには rbs_rails と連携するための設定も含まれています。

# Steepfile

target :app do
  signature 'sig'

  check 'app'

  library 'pathname'
  library 'logger'
  library 'mutex_m'
end
実行

さきほど出てきた Book モデルに user_name というメソッドを定義します。 まずは型定義から。rbs_rails で生成した book.rbs に追記します。

# sig/app/models/book.rbs

class Book < ApplicationRecord
  ...
  
  def user_name: () -> String
end

book.rb の方で user_name メソッドを定義して、その中で namenaem と間違えてみます。

# app/models/book.rb

class Book < ApplicationRecord
  belongs_to :user
  
  def user_name
    user.naem
  end
end

型検査を実行してみると

$ bundle exec steep check
app/models/book.rb:5:4: NoMethodError: type=::User, method=naem (user.naem)

検出できたました。user に naem というメソッドは定義されていないよと教えてくれています。

次に、返り値の型を間違えてみます。

# app/models/book.rb

class Book < ApplicationRecord
  belongs_to :user
  
  def user_name
    user.id
  end
end
$ bundle exec steep check
app/models/book.rb:4:2: MethodBodyTypeMismatch: method=user_name, expected=::String, actual=::Integer (def user_name)
  ::Integer <: ::String
   ::Numeric <: ::String
    ::Object <: ::String
     ::BasicObject <: ::String
==> ::BasicObject <: ::String does not hold

これも検出できました。継承関係をたどった結果、返り値が String クラスではないと判断されたことがわかります。

この他、Rails ではないものの Steep のリポジトリの smoke ディレクトリ にサンプルがたくさんあるのでここを見てみるのも面白いと思います。

エディタによるサポート

静的型があることにメリットのひとつとして、エディタによる補完が強力になるというのがあると思います。

現時点では、VSCode では Steep type checker というエクステンションをインストールすることで型情報を表示できます。

↓gif を作ってみました

f:id:natsuokawai:20200828154042g:plain
vscode でのコード補完

ActiveRecord のメソッドを型情報付きで表示してくれています。

また補完とは関係ないですが、ruby-signature というエクステンションを入れることで RBS ファイルにシンタックスハイライトが効くようになるのでこちらもおすすめです。

まとめ

Rails × 型についてかんたんに紹介してみました。 今回のコードは Github に上げてあるのでよかったら遊んでみてください。

最後に、株式会社スタメンでは一緒にプロダクトを作っていくメンバーを募集しています。
ご興味のある方はエンジニア採用サイトをご覧ください。

参考