型のあるRails開発を試してみる(2021/01/07)
試してみたログです。
前提知識
rubyの型周りをキャッチアップできてない人は、この辺を読んでおくと良い。
rbs,typeprof,steepを試してみる
参考サイトは以下(以下の通りにやっただけなので詳細は省略)
準備
$ ruby -v
# ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin19]
$ gem install steep
実際
以下の実装を元に説明する。
def plus(x, y)
x + y
end
p plus(1, 2)
まず型情報であるrbsファイルを typeprof
で生成する。
$ typeprof app.rb -o sig/app.rbs
rbsの中身は現状だと以下のようになる。
# Revealed types
# app.rb:5 #=> Integer
# Classes
class Object
private
def plus: (Integer x, Integer y) -> Integer
end
次に、 steep init
コマンドを使うとSteepfileが生成されるので、これを以下のように編集する。
Steepfileの記法を詳しく解説しているページが見つからなかったが、pockeさんの説明を以下の通りに書く。
target :lib do
# rbsファイルがあるディレクトリ の指定
signature "sig"
# 解析したいrubyコードのディレクトリ/ファイル
check "app.rb"
# (以下、今は不要だが一旦載せておく)
# gem_rbs 内の gems ディレクトリへのパスを指定
# repo_path "gem_rbs/gems"
# rbs gem, gem_rbs からrequireしたいライブラリ名を指定
# library 'pathname'
end
ここまでで、型検査ができるようになる。
$ steep check
# エラーがなければ、何も表示されない
plus
に渡す引数を文字列などにすると、検査時にエラーとなる。
$ steep check
# app.rb:5:10: ArgumentTypeMismatch: receiver=::Object, expected=::Integer, actual=::String ("a")
vscodeでの補完を試してみる
vscodeのextensionsでsteepを検索してインストールすればOK。
ドキュメントの通りだが以下に注意。以下が適切に行われていないと何もチェックされない。
-
bundle exec steep langserver
を実行して型チェックを行うため、このコマンドが実行できるようGemfileが必要 - ルートフォルダにSteepfileが必要
インストールすると、エディタ上で補完・検査されるようになった!
rbs_railsを試してみる
参考サイトは以下(以下の通りにやっただけなので詳細は省略)
一応、手順通りに作ったレポジトリも公開しておく。
rbsの生成
手順は参考サイトを参照。以下、詰まった点(2021年1月7日現在。rbs_railsのバージョンは0.8.0)
-
rails new rails_with_rbs_rails_sample --api
した後にbin/rake rbs_rails:all
すると各種のエラーが発生するので、不要なライブラリを一旦無視する
- ブログの通り、
bundle exec rbs -rlogger -rpathname -rmutex_m -rdate --repo=gem_rbs/gems -ractivesupport -ractionpack -ractivejob -ractivemodel -ractionview -ractiverecord -rrailties -I sig validate
しようとすると、以下エラー。
/Users/foo/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/rbs-1.0.0/stdlib/logger/0/log_device.rbs:3:4...3:24: Could not find mixin: MonitorMixin (RBS::NoMixinFoundError)
後続のSteepはそれなりに動くので、一旦無視する。
steepの実行
$ bundle exec steep check
app/mailers/application_mailer.rb:2:2: NoMethodError: type=singleton(::Object), method=default (default from: 'from@example.com')
app/mailers/application_mailer.rb:3:2: NoMethodError: type=singleton(::Object), method=layout (layout 'mailer')
railsディレクトリを型検査できた。意外とエラーが少なく素晴らしい :clap:
上記は、action_mailerで型定義が足りてないエラーか。
自分でモデルを適当に生成してみる。
$ bin/rails generate model post title:string body:text
$ bin/rails db:migrate
生成した段階だと、Postクラスに相当するrbsファイルがないので型検査は行われない。再度rbsを生成する。
ちなみに、rbs_railsは実際にrailsを実行してrbsファイルを生成するため、例えばrailsでMySQLを使っていればmysqlも起動しておく必要がある。
$ bin/rake rbs_rails:all
Postクラスに適当に実装する
class Post < ApplicationRecord
def foo
title.split(',')
end
end
$ bundle exec steep check
# ...mailerのエラーは省略
app/models/post.rb:3:4: NoMethodError: type=(::String | nil), method=split (title.split(','))
titleがnilableなのでエラーになった!良い感じ。
vscodeのsteep extensionでこの型情報を反映させるには、都度restartが必要なようなので、vscodeのコマンドパレット(Shift+Cmd+P)より Steep: Restart all
する。
ちなみに、 title&.split
のように書くとエラーは防げるのだが、vscode上の補完だとstring型のメソッドが候補に上がってこないので使いづらそうだった。
コントローラーを実装してみても問題ないことを確認
class PostsController < ApplicationController
def show
post = Post.last
{
title: post.title # 型が補完されている
}
end
end
型定義を追加していく
ここまでは rbs_rails
が自動で生成してくれる型定義しか使えず、例えば上記の Post#foo
のような自作のメソッドは、自分で型を追加する必要がある。
まずブログで紹介されている通り、rbs prototype
を使って生成してみる。
# サンプル実装
class Post < ApplicationRecord
def foo
[1].map { |x| x + 1 }
end
def bar
title
end
end
$ bundle exec rbs prototype rb app/models/**/*.rb > sig/models.rbs
出力結果は以下の通り。prototypeの名前の通り、自力で型定義をしていく上での原型を生成してくれる(=戻り値は単純なケースを除き基本的にuntypedとなる)。
# sig/models-prototype.rbs
class ApplicationRecord < ActiveRecord::Base
end
class Post < ApplicationRecord
def foo: () -> untyped
def bar: () -> untyped
end
typeprofでも同じくやってみると以下。fooについては型が付く。
$ typeprof app/models/**/*.rb -o sig/models.rbs
# sig/models-typeprof.rbs
# Classes
class ApplicationRecord
end
class Post < ApplicationRecord
def foo: -> Array[Integer]
def bar: -> untyped
end
Foo#bar
は Foo#title
を利用しているので、titleの型定義が書かれた sig/rbs_rails/app/models/post.rbs
をtypeprofで参照できれば良さそうだが、この方法が分からなかった。。
(2021/01/10追記) typeprofのドキュメントに普通に書いてあった :bow:
以下でいけそうだが、現状だとtypeprofのエラーになる。(また、もう少し良い感じにrbsファイルを指定できそう)
$ typeprof gem_rbs/gems/**/*.rbs sig/rbs_rails**/*.rbs app/models/post.rb -o sig/post.rbs
/Users/foo/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/typeprof-0.11.0/lib/typeprof/import.rb:262:in `each_class_decl': undefined method `decls' for nil:NilClass (NoMethodError)
issueを立てておいた。
一旦barを自力で定義すれば問題なく型チェックが行えるので、次に進む。
実際のRailsアプリに入れてみる
上記の手順を運用中のrailsアプリに実行してみる(手順は省略)。
以下エラーとなった。
$ bundle exec steep check
sig/rbs_rails/app/models/report.rbs:168:2...168:46 DuplicatedMethodDefinitionError: class_name=::Report, method_name=actual
sig/rbs_rails/app/models/report.rbs:178:4...178:43 DuplicatedMethodDefinitionError: class_name=::Report::ActiveRecord_Relation, method_name=actual
エラーの名前の通り、定義が重複しているらしい。コードをみてみると、enumとscopeで二重にメソッド定義しているコードがあったので修正。こういうのは型検査ありがたい(重複定義とか滅多にしないが)(とか言いつつしてしまっていた顔)
修正後再度型検査すると、大量にエラーがでる。ほぼNoMethodErrorなので、いったんtypeprofで一通り自作のメソッドを定義してみる。
と、再度同じようにrbsでいくつかエラーが出る。
- enumをオーバーライドしていると、
DuplicatedMethodDefinitionError
になる。そもそもオーバーライドするなという話ではありつつ、どうしてもその必要があったときに、この型の修正が面倒臭そう。 -
InvalidTypeApplicationError
よく分からないが、EnumerableやRangeなどでエラーになっていた。この辺のオブジェクトを読み込めていないためか?一旦消して対応。
これを乗り越えても大量に NoMethodError
がでるが、残りのエラーをざっくりみた限り、以下っぽい。
- 依存gemのrbs不足(これがかなり多そう)
- よくわからない理由のエラー(steepがダメなのか、こちらが何かしらダメなのか判別できなかった)
まとめ
ようやくrubyでも型安全な開発の未来が少し見えてきたかなという感じ。実用でいうとまだだと思うが、応援していきたい(rbs界隈の方々に本当に感謝!)。
いちユーザーとして、とりあえず目の前で必要そうと感じたのは以下。
- (型定義を極力書きたくない前提で)rbs_railsでrbs生成->typeprofで適当な形でrbs生成->どうしても必要な型だけ書く->steepで自動で読み込んでくれてエディタに反映 というフローが綺麗に回るようにしたい
- 依存gemのrbs
- また、rds定義が不足している場合にサクッと追加できるようにrubiestのrbs力が高まらないと厳しそう
Discussion