こんにちは。スタメンの河井です。
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つの機能を持っています。
- Rails が用意してくれているクラスの型定義ファイルの生成
- ユーザーが定義したモデルクラスの型定義の生成
rbs_rails#usage に従って Rake Task を実行します。
これによって生成される Rails の型定義ですが、たとえば ActiveRecord_Relation の型はこのようになります。
次はモデルからの型生成です。 たとえば次のような 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
このような型定義が生成されます。
カラム名に応じて動的に定義されるメソッドや、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 メソッドを定義して、その中で name
を naem
と間違えてみます。
# 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 を作ってみました
ActiveRecord のメソッドを型情報付きで表示してくれています。
また補完とは関係ないですが、ruby-signature というエクステンションを入れることで RBS ファイルにシンタックスハイライトが効くようになるのでこちらもおすすめです。
まとめ
Rails × 型についてかんたんに紹介してみました。 今回のコードは Github に上げてあるのでよかったら遊んでみてください。
最後に、株式会社スタメンでは一緒にプロダクトを作っていくメンバーを募集しています。
ご興味のある方はエンジニア採用サイトをご覧ください。