こんにちは,相生ゆら(@Little_Rubyist)です.最近配信でクロノトリガーをやっているんですがあまりにも面白すぎてありとあらゆる知人に「クロノトリガーをやれ」と言っています.
さて,入社から半年以上経過しましたがその話はまた別でやるとして,私の所属している部署では勉強習慣というのがあって,自分の興味のあることを業務時間中に勉強してもいい時間が存在します.各々色んなことをやっていますが,私はその時間で少しずつRBSを導入しています.まだあまり触れていないプロジェクトなのでコードリーディングをしながらゆっくり入れていこうと思っています.
CI導入まで済んだのでこれを一区切りとし,どういったことをやっていたのかをご紹介します.
型導入の準備
とってもシンプルです.steep, rbs_rails, katakata_irbをGemfileに追加します.ライブラリの型も必要なので,rbs collectionを用いてgem_rbs_collectionを追加します.
katakata_irbはproduction環境のrails consoleを早くしたいという意図もあるのでproductionでも使えるように設定します.
(2023-10-05追記)元々irbに対して補完メソッドの読み込みが重たいなという感想を抱いており,katakata_irbが型を見てくれることで補完の読み込みが早くなる(特にHoge..
と誤字してしまった時など)効果がありました.
プロダクトコードに型を定義していない状態で導入してもgemの型や標準ライブラリの型がそれなりにあるので,早期にrbs導入の恩恵を得られることも大きなメリットです.
ちなみに,rbsは3系からは標準で入っているので steepなどにより依存関係として自動でインストールされるので*1,Gemfileに書く必要はありません.
# Gemfile gem 'katakata_irb' group :development, :test do gem 'rbs_rails', require: false gem 'typeprof', require: false gem 'steep', require: false end
gem_rbs_collectionはGem本体のリポジトリに型が書かれていないGemのためのリポジトリです.Gemfileから依存関係などを解析して,各Gemに対応したRBSファイルがあればそれを手元にダウンロードします.
まずはrbs_collectionの設定ファイルを用意して,そのままRBSファイルをダウンロードします.
$ rbs collection init $ rbs collection install
RBSファイルは path:
で設定された場所にダウンロードされます.ダウンロードされるファイルは沢山あるのでgitから除外しておきます.
$ echo /.gem_rbs_collection/ >> .gitignore
基本は特に編集せずにrbs collection install
を行います.Gemなどで依存している標準ライブラリは現状検知できないので,そういったライブラリは rbs_collection.yaml
に記載していくことになります.
# 例 # rbs_collection.yaml gems: - name: erb
この時点でライブラリの型付けは概ね完了になります.用意されていないものについては自身で gem_rbs_collectionにcontributeしたり,手元に秘伝の型*2を用意する必要があります.
自動生成出来る型は自動生成する
自動生成の手段はいくつかあります.rbsに用意されているprototypeやtypeprof, railsアプリならrbs_railsを用いるとよいでしょう.*3
今回は少しずつ入れたいのでrbs_railsはmodelだけ*4,prototypeもmodel層のみを対象に実行します
$ bin/rails g rbs_rails:install $ rails rbs_rails:generate_rbs_for_models
今回のプロジェクトにはcomposite_primary_keysが入っており素直に実行できなかったのでモンキーパッチを当てています.
どうせモンキーパッチを当てるならと,models層に書かれた attributes
が自動生成出来るように追加でパッチを書きました.*5
prototypeではsig/protorype
以下に app/models/...
を生成します.
$ bundle exec rbs prototype rb --out-dir=sig/prototype --base-dir=app app/models
その他にもgemの中で自動生成できそうなものがあればrbs_railsに相乗りすることで生成が可能なので少しだけ生成しました.*6
rbs validateを通す
rbsには正しく記述されているか確認するコマンドがあります.「正しい記述」には呼び出されているのに定義のないmoduleやclassなども含むのでそういったものを追記していきます.
おかしなコードを書いていると偶におかしな型が生成されるので,そういったものは手で修正をします.
例えば
# rubyの予約語と衝突するのを避けるためにおかしなことをしているコード alias original_hash hash alias hash original_hash
がプロダクトコードにかかれていると,型も
alias original_hash hash alias hash original_hash
になってしまいます.しかし,これだと Circular method alias is detected
と怒られてしまうので(当たり前)
def original_hash: () -> untyped def hash: () -> untyped
にして誤魔化します.必要であれば後からuntyped
な部分は修正します.
rbs validateが通ると何も対象に取っていないsteep checkが通るので,CIに組み込むことが可能になります.
CIに組み込む
ここではCircleCIを採用しているので,以下のようなconfigを書いて任意のworkflowにsteep checkを組み込みましょう.
steep_check: executor: ruby steps: - run: name: configure bundler command: | gem install bundler - run: name: rbs collection install command: | bundle exec rbs collection install - run: name: steep check command: | # steepはjob数とvCPUを一致させる必要がある # なので、CircleCIの`resource_class`(vCPU数)に応じてジョブ数を変更させる VCPU_NUM=`echo $(($(cat /sys/fs/cgroup/cpu/cpu.shares) / 1024))` bundle exec steep check --jobs=$VCPU_NUM
gem_rbs_collectionはgitignoreしているのでCIの中でも忘れずにinstallする必要があります.
また,コメントにあるようにjob数をvCPUの数と一致させる必要があります.これはずっとCIが通らなくて,諦めてruby-jpで相談して教えていただきました.vCPU数よりjob数が大きくなってしまうとThreadがエラーを吐いてタスクが終わらなくなるようです.
job数を指定しない時に何故そういう事が起こってしまうかというと,steepはConcurrent.physical_processor_count
を使ってデフォルトのjob数を計算しているようで,手元で実際に確認したところvCPU数が2に対してConcurrent.physical_processor_count
は18で乖離していました.
ローカル環境ではどちらも2だったので乖離は発生せず,steep checkも問題なく動いたのでCIなどのコンテナ環境でのみ起こるものだと思われます.
他の方の話では「vCPU数を超えた値を --job
で指定すると ThreadのErrorが起きた記憶があります。」とのことだったので,そのような症状の場合にも同じ対応で解消するかもしれません.
ちなみに,この件を相談した時にsteep開発者であるsoutaroさんが対応を考えてくれていたので,近い将来にこの問題は根本的に解決する可能性があります.
(2023-10-05追記)今回の記事を報告した際に対応をしていただきました!内容としては「環境変数に CI
があった時にjobのデフォルト数を2にする」というものです.soutaroさんありがとうございます… :pray:
まとめ
最終的にはruby-jpの皆さんを頼りましたが,何はともあれ無事にRBSを用いたRubyの型チェックをCIに導入することが出来ました.Rubyは動的型付けの言語ですが,型による補助で安全なコードがもっと沢山書けるといいなと思っています.これから色んなメソッドに型を付けていく作業が待っているんですが,それは少しずつやっていこうと思います.
最後に
我々は新しいことに一緒に挑戦してくれるようなエンジニアを歓迎しています.新しくないことをやってくれるエンジニアも歓迎しています.興味のある方は下記採用ページからご連絡ください!*7