Header image

クラッソーネの開発者がエンジニアリングに関することもそうでないことも綴っています!

自作のGemにTypeprofとSteepを使った型検査の仕組みを導入してみた

自作のGemにTypeprofとSteepを使った型検査の仕組みを導入してみた

こんにちは、プロダクトマネージャーの山口拓弥(@yamat47)です。

しばらく記事を書かないうちに Rails エンジニアから役割が変わっていました。
この辺りのことは誕生日に書いた記事に書いているので、よければお読みください:

https://blog.yamat47.me/entry/2022/06/02/002443

さて、この記事では Ruby 3 から本格的に導入された型解析の仕組みについて書いてみます。
ただ 遠藤さんのまとめ記事 をはじめとして型解析自体について解説されたものはすでにありますので、実際に試してみた体験談をまとめてみます。

Ruby での型解析って?

Ruby は実行するまで型が決まらない、いわゆる動的型付け言語です。
しかし TypeScript の流行や Python での型注釈記法の導入をはじめとして、動的型付け言語であっても型検査の仕組みを導入するような動きが活発です。
そして Ruby においても、2020 年 12 月にリリースされた Ruby 3.0 にて静的型解析を行う仕組みが導入されました。

Ruby の型解析には RBS Typeprof Steep Sorbet というツールが登場します。
遠藤さんのまとめ記事からの引用ですが、それぞれの役割はこういったものです:

  • RBS: Ruby 3 の型情報を扱う言語を始めとする基盤。Ruby 3 にバンドルされる。
  • TypeProf: 型注釈のない Ruby コードを型解析するツール。Ruby 3 にバンドルされる。現状の主機能は Ruby コードからの RBS スタブ生成。
  • Steep/Sorbet: Ruby の静的型検査器。型注釈を書く必要はあるが、Ruby で静的型の便利なプログラミング体験ができる。IDE での補完やドキュメント表示も。

source: Ruby 3の静的解析機能のRBS、TypeProf、Steep、Sorbetの関係についてのノート - クックパッド開発者ブログ

今回、自作の Gem について Steep を使った型検査の仕組みを導入してみました。
「Ruby の型検査って聞いたことあるけど使ったことないな」という方に参考にしていただけると嬉しいです。

自作の Gem に静的型検査の仕組みを導入してみた

試してみたライブラリの紹介

今回型解析を導入してみたのは yamat47/japanese_address_parser という Gem です。
以前このブログでもご紹介したことがある、Ruby で日本の住所を解析するためのライブラリです:

https://tech.crassone.jp/posts/introduction-for-japanese-address-parser-gem

このライブラリは JapaneseAddressParser という Module を提供していて、その大まかな使い方はこんな感じです:

# `.call` というインターフェースを持ちます。
address = JapaneseAddressParser.call('東京都港区芝公園4-2-8')

# `.call` の返り値は JapaneseAddressParser::Models::Address というクラスです。
# 都道府県・市区町村・町域の情報を持っています。
address.class #=> JapaneseAddressParser::Models::Address

# JapaneseAddressParser::Models::Address#address は住所のふりがなを表す文字列を返します。
address.furigana #=> "トウキョウトミナトクシバコウエン 4"

とりあえず .rbs ファイルを自動で生成してみる

Steep を使って型解析をするためには、型の情報がまとめられた .rbs ファイルが必要です。
これは Typeprof を使って自動で生成することができます。

bundle exec typeprof lib/**/*.rb spec/**/*_spec.rb -o sig/japanese_address_parser.rbs

これを使うと、こんな感じの .rbs ファイルが自動で生成されました。

sig/japanese_address_parser.rbs
# 他にもたくさんの定義が出力されていましたが省略しています。
module JapaneseAddressParser
  VERSION: String

  def call: (untyped full_address) -> Models::Address?
  def call!: (untyped full_address) -> Models::Address
  def _call: (untyped full_address) -> Models::Address
  def self.call: (untyped full_address) -> Models::Address?
  def self.call!: (untyped full_address) -> Models::Address
  def self._call: (untyped full_address) -> Models::Address
end

Steep の初期設定をして動かしてみる

RBS を使った型検査には Steep を使います。
bundle exec steep init で初期設定をしつつとりあえず steep check を実行してみると、こんな感じのエラーが出てしまいました:

$ bundle exec steep check
# Type checking files:

..................................F...............................

sig/japanese_address_parser.rbs:18:4: [error] Cannot find type `Schmooze::Base`
│ Diagnostic ID: RBS::UnknownTypeName
│
└     class NormalizeJapaneseAddressesSchmoozer < Schmooze::Base
      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Detected 1 problem from 1 file

これは Schmooze::Base というクラスに対する型定義が見つからないというエラーで、依存するライブラリの情報が不足していたのが原因です。
Steepによる型チェックの実践例 - Blog - @ybiquitous を参考にしつつ、こちらのコマンドで Schmooze の型定義を追加しました:

bundle exec rbs prototype runtime -r schmooze Schmooze::Base >> sig/schmooze_base.rbs

これで steep check による型検査が実行できるようになりました。

steep check をヒントに型定義やコードを修正していく

自動で生成された型定義は untyped なところも多く、このままではあまり実用性は高くありません。
steep check を頼りに .rbs ファイルやコードを直していきます。

今回はとてもシンプルなインターフェースを持つライブラリを扱ったため、修正箇所はあまりありませんでした。
唯一あったのは Hash のキーを文字列ではなくシンボルにしてしまったときで、Steep がこんなエラーを吐いてくれました:

$ bundle exec steep check
# Type checking files:

........................................................................................................................................................F.

spec/japanese_address_parser_spec.rb:11:37: [error] Cannot pass a value of type `::Symbol` as an argument of type `::String`
│   ::Symbol <: ::String
│     ::Object <: ::String
│       ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::ArgumentTypeMismatch
│
└         ::JapaneseAddressParser.call(:hoge)

ちなみに型解析に成功したときはこんな感じの結果が出力されます:

$ bundle exec steep check
# Type checking files:

..........................................................................................................................................................

No type error detected. 🍵

修正を繰り返して、最終的にはこんな型定義になりました:

https://github.com/yamat47/japanese_address_parser/blob/2135210039e3ae6290859b57cfcaa831a963fb4b/sig/japanese_address_parser.rbs

CI でも型検査を実行する

このライブラリは GitHub Actions を使った CI を設定しています。
せっかくなので、今回準備した型検査も CI で実行するようにしました。

.github/workflows/main.yml
+    - name: Steep check
+      run: bundle exec steep check

以上、自作のライブラリに型検査を導入してみた体験談でした。

今後やりたいこと

先日の RubyKaigi 2022 でも言及されていましたが、Ruby にはさまざまな Gem の型定義を集めた gem_rbs_collection というリポジトリが存在します。
ここにある型定義を RBS から利用することで、Rails のように大量のライブラリに依存するようなプロジェクトでも手間を少なく型検査を実行できるようになります。

https://github.com/ruby/gem_rbs_collection

しかし現状では全体の 1% 未満のライブラリについての型定義しか登録されておらず、まだまだ実用化にはほど遠い状態です...。
「Rails アプリに Steep での型検査を入れてみた!」という事例はだんだんと増えていますが、どの記事もなかなかに苦労しているのが現状です。

この辺りの話は RubyKaigi 2022 のふーがさんの発表がとても参考になります:

今回せっかく型定義を追加してみたのでgem_rbs_collection にも型定義を追加してみようと思います。
(そこまでやっても良かったのですが、テストの書き方とか程度がわからず後回しにしちゃってます)

まとめ

この記事では、シンプルな Ruby のライブラリに Steep による型検査を導入してみました。
とても楽しかった一方でまだまだ情報はなかなかなく、VSCode などの IDE に導入するのも一苦労です。
この記事が「Ruby の型解析に興味はあるけどよくわからん」という方に少しでも参考になれば嬉しいです。

クラッソーネでは言語・技術の先端を追いかけるエンジニアを大募集しています。
ご興味ある方はぜひ採用サイトをご覧ください!

https://www.crassone.co.jp/recruit/engineer/


クラッソーネで Ruby on Rails を使ってサービス開発をしています。週末はアメフトのコーチをしたり選手もしたり、たまに審判もしたりしてます。エビ中もすき。