個人的に今年から毎週読ませていただいている週間Railsウォッチにて、「Sorbet」というGemが紹介されました。
※アイコンかわゆい。
よければプロダクトにも導入してみたいな〜と思い、軽い気持ちで触ってみました。
この記事なに? 🙄
この記事では、Gem「Sorbet」をRuby on Rails環境に導入、軽く動作確認した際の手順と自分なりに咀嚼した内容を記載します。
もしよろしければ、一読・参考にしていただければと思います。 🙇♂️
Sorbetとはなんぞや? 🤔
Sorbetとは、Rubyの型チェックを行ってくれるGemのことです。
ざっくり解釈した内容は以下な感じです。
- 型チェックとは何をしてくれるのか
- 未定義のクラス名を記述したらエラー検出してくれる
- メソッドの先頭に型定義(引数や戻り値の型を記述)することで、メソッド呼び出し側の型に誤りがないかチェックしてくれる
- 使用イメージ
もっと正確に概要を知りたい方は、週間Railsウォッチでも紹介されていた、RubyKaigi2019のスライドを見てみると良いかなと思います。
※全編英語ですが、英語力が低い自分でもさっくり内容を理解することができましたので、一読推奨です。 👍
本編(導入〜動作確認)
実際に以下PR(ブランチ)で試したことを書いていきます。
環境
以下の環境を使用しました。
導入
公式のGET STARTEDから進むといい感じです。
sorbetの初期化
以下をGemfileに追記します。
gem 'sorbet', :group => :development gem 'sorbet-runtime'
その後、以下で「sorbet」を導入&初期化します。
bundle install # bundle install終了後... bundle exec srb init
すると、以下のような初期設定が行われます。
sorbet/
ディレクトリが生成、配下にsorbetの設定情報が出力される- 各rbファイルの先頭へ
typed: 〜
が挿入される
詳しい内容は、こちらのコミットログ - srbの初期化を参照ください。
※
typed: 〜
については後ほどちょろっと記載します。
sorbet配下に生成されるrbiファイルについて
Gemfile
に記述、require
したGemに対して、型チェックに必要な情報を解析→自動出力してくれているようです。
rbiファイルを更新するには、bundle exec srb rbi gems
でいける感じのようです。
型チェックの処理を記述
型チェックは以下の実装を行う必要があるっぽいです。
- 該当する
class
/module
にT::Sig
を継承させる typed: 〜
をtrue
/strict
/strong
のいずれかに設定typed: 〜
の〜
に設定する値は File-level granularity: strictness levels を参照
signature
をメソッドの先頭に定義- メソッドやパラメータの型定義方法はこちら参照
実際に実装した内容は以下です。
# app/controllers/application_controller.rb # typed: strong class ApplicationController < ActionController::Base extend T::Sig private sig {params(x: Integer).returns(String)} def sorbet_test_to_s_app(x) x.to_s end end # app/controllers/pages_controller.rb # typed: true class PagesController < ApplicationController extend T::Sig # typed: strong だとsignatureを定義しないとエラーとなる def about # コメント化されている部分はSorbetによる型チェックエラーとなる @value = '' @value += sorbet_test_to_s CONST_INT @value += sorbet_test_to_s CONST_FLOAT # @value += sorbet_test_to_s CONST_STRING @value += sorbet_test_to_s_app CONST_INT # @value += sorbet_test_to_s_app CONST_FLOAT # @value += sorbet_test_to_s_app CONST_STRING end private CONST_INT = 123 CONST_FLOAT = 45.6 CONST_STRING = '7-8-9' sig {params(x: T.any(Integer, Float)).returns(String)} def sorbet_test_to_s(x) x.to_s end end
T.any
についてはUnion Typesを参照ください。
詳しい内容は、こちらのコミットログを参照ください。
動作確認
では実際に動作確認をしていきます。
型チェックを実行
ソースコードを以下の状態にして、型チェックを実行します。
# app/controllers/application_controller.rb # typed: strong class ApplicationController < ActionController::Base extend T::Sig private sig {params(x: Integer).returns(String)} def sorbet_test_to_s_app(x) x.to_s end end # app/controllers/pages_controller.rb # typed: true class PagesController < ApplicationController extend T::Sig # typed: strong だとsignatureを定義しないとエラーとなる def about # コメント化されている部分はSorbetによる型チェックエラーとなる @value = '' @value += sorbet_test_to_s CONST_INT @value += sorbet_test_to_s CONST_FLOAT @value += sorbet_test_to_s CONST_STRING @value += sorbet_test_to_s_app CONST_INT @value += sorbet_test_to_s_app CONST_FLOAT @value += sorbet_test_to_s_app CONST_STRING end private CONST_INT = 123 CONST_FLOAT = 45.6 CONST_STRING = '7-8-9' sig {params(x: T.any(Integer, Float)).returns(String)} def sorbet_test_to_s(x) x.to_s end end
# 型チェック実行 bundle exec srb tc # 結果 app/controllers/pages_controller.rb:11: Expected T.any(Integer, Float) but found String("7-8-9") for argument x https://srb.help/7002 11 | @value += sorbet_test_to_s CONST_STRING ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/controllers/pages_controller.rb:30: Method PagesController#sorbet_test_to_s has specified x as T.any(Integer, Float) 30 | sig {params(x: T.any(Integer, Float)).returns(String)} ^ Got String("7-8-9") originating from: app/controllers/pages_controller.rb:21: 21 | CONST_STRING = '7-8-9' ^^^^^^^^^^^^ app/controllers/pages_controller.rb:13: Expected Integer but found Float(45.600000) for argument x https://srb.help/7002 13 | @value += sorbet_test_to_s_app CONST_FLOAT ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/controllers/application_controller.rb:13: Method ApplicationController#sorbet_test_to_s_app has specified x as Integer 13 | sig {params(x: Integer).returns(String)} ^ Got Float(45.600000) originating from: app/controllers/pages_controller.rb:20: 20 | CONST_FLOAT = 45.6 ^^^^^^^^^^^ app/controllers/pages_controller.rb:14: Expected Integer but found String("7-8-9") for argument x https://srb.help/7002 14 | @value += sorbet_test_to_s_app CONST_STRING ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/controllers/application_controller.rb:13: Method ApplicationController#sorbet_test_to_s_app has specified x as Integer 13 | sig {params(x: Integer).returns(String)} ^ Got String("7-8-9") originating from: app/controllers/pages_controller.rb:21: 21 | CONST_STRING = '7-8-9' ^^^^^^^^^^^^ Errors: 3
signature
に定義している内容に反している、以下実装がエラー検出されていることが確認できています。
@value += sorbet_test_to_s CONST_STRING @value += sorbet_test_to_s_app CONST_FLOAT @value += sorbet_test_to_s_app CONST_STRING
rails s
→動作確認
先ほどのソースの状態のまま、bundle exec rails s
で起動、http://localhost:3000/about
(pages#about)をブラウザで開いてみます。
すると、以下のようにエラー検出されていることが確認できます。
問題となっている以下コード or signature
をコメント化すると、問題なく表示できることが確認できます。
# 問題のコード @value += sorbet_test_to_s CONST_STRING @value += sorbet_test_to_s_app CONST_FLOAT @value += sorbet_test_to_s_app CONST_STRING
実行時エラーを無視するには?
この仕組みはGemfile
に記述したsorbet-runtime
によって実現されているようです。
コードをより良くするため、この仕組みはとても良いと思うのですが、
- ローカル or テスト環境では有効 他(ステージング、本番)環境では無効に設定
のような運用方法が考えられます。
上記のようなことは、Runtime Configuration · Sorbetから設定できそうな感じに見受けられるのですが...それはまた別の機会に試そうかなと思います。
おわり
「sorbet」=「ソルベ」って読むんですね...。
※ソルベット(sorbetto)と読んでいた。
いい加減英語覚えないとなーと思う、思っている。
間違い等ありましたら、FBいただけると幸いです。🙇♂️
参考記事
- Adopting Sorbet in an Existing Codebase · Sorbet
- 速報: Ruby向け型チェッカー「Sorbet」をStripeがオープンソース化
- Rubyの型解析ライブラリSorbet事始め - Qiita
所感
テストをしっかり書いてなかったりすると、Ruby on Railsのコードはどんどんメンテしにくいものになっていく傾向があると感じています。
上記のような問題点を改善していくために、このsorbet
は有効(最高)だな...と感じています。
(sorbet配下に出力されるファイルが多いため、CIで回すとかがいいかもですね。)
まだ制約(include
したメソッドは対象外となる...?)もあるっぽいので、更新状況や使い勝手を試しながら、プロダクト開発へ導入提案するか、検討しようかなと思います。