はじめに
イタンジ株式会社で物件管理くんの開発をしている河合です。最近、ミニシアターで「怪物」という映画を観てきました。作品の世界観と映画館の雰囲気が相まって、映画に没入できとてもいい体験になりました。
物件管理くんでは、フロントはTypeScript、バックエンドはRubyで開発を行なっています。最近チーム内でRubyにも型を導入しようという話になり、RBSとSteepを触ってみたため、今回はそれを記事にしようと思います。
型を導入するメリット
エラーの機械的検出
静的型検査によって、開発者が意図していない操作(型に関するエラー)を機械的に検出することができます。加えて型定義を変更すれば、静的型検査時に修正の必要な箇所が型エラーとして現れるため、修正箇所の特定が容易になります。
開発体験の向上
インターフェースとして型が定義されていれば、コードの可読性が向上し、リファクタリングも容易になります。
導入方法
ライブラリ
インストール
1.GemfileにRBSとSteepを追加し、ターミナルで$bundle install
を実行しました。
gem "rbs" gem "steep"
2.ターミナルで$steep init
を実行し、Steepfileを作成しました。
D = Steep::Diagnostic target :lib do signature "sig" check "lib" configure_code_diagnostics do |hash| hash[D::Ruby::MethodDefinitionMissing] = :warning end end
configure_code_diagnostics
で、型検査のエラーレベルをカスタマイズすることができます。RBSで宣言されているにも関わらず、Rubyに実装されていないメソッドがあればwarningを出したいため、MethodDefinitionMissing
を指定しています。
3.ターミナルで$steep check
を実行し、型検査が動作するかテストしました。
$steep check => No type error detected.
触ってみた感想
公称型、構造的部分型を使い分けながら、多くのケースで型付けすることができ、型の恩恵を十分に得ることができると思いました。ただし、動的なメソッドなど検査型することができないケースも存在するため、型検査に頼りすぎないよう注意しようと思います。
公称型
型にクラス名を指定すると、公称型によって型の互換性を検証します。RubyはTypeScriptと違い、オブジェクトが持つメソッドが動的に変化することがあるため、公称型を採用しているのではないかと思っています。
## rbs class Cat attr_reader name: String def initialize: (String) -> void end class Dog attr_reader name: String def initialize: (String) -> void end class Object def put_name: (Cat) -> void end ## ruby(クラスやメソッドの実装は省略) dog = Dog.new("Pochi") put_name(dog) ## ターミナル $ steep check => Cannot pass a value of type `::Dog` as an argument of type `::Cat`
構造的部分型
クラスに関係なく、オブジェクトが特定のメソッドを持っていれば、型の互換性ありと判断したい場合があると思います。 そんな時は、Interfaceを用いることで、構造的部分型で型の互換性を検証することができます。
## rbs interface _Foo def foo: () -> String end interface _Bar def bar: () -> String end class Foo include _Foo end class FooBar include _Foo include _Bar end class Object def put_foo_bar: (_Foo & _Bar) -> void end ## ruby(クラスやメソッドの実装は省略) put_foo_bar(Foo.new) # fooメソッドしか持っていないため型エラー put_foo_bar(FooBar.new) # foo, barメソッドを持っているためOK ## ターミナル $ steep check => Cannot pass a value of type `::Foo` as an argument of type `(::_Foo & ::_Bar)`
動的なメソッド
動的なメソッドに対しては型検査を行うことはできません。
@dynamic
アノテーションを使うことで、動的なメソッドが実装されていることをSteepに検知させることはできます。
## rbs class Sample def dynamic_method: () -> Integer end ## ruby class Sample # @dynamic dynamic_method define_method :dynamic_method do "dynamic_method" end end puts Sample.new.dynamic_method # 動的なメソッドの型検査は行われないため、型エラーにならない ## ターミナル $ steep check => No type error detected. # 動的なメソッドが実装されていることをSteepが検知しているため、MethodDefinitionMissingにはならない
まとめ
Rubyに型を導入するにあたり、RBSとSteepを触ってみました。Rubyの動作を阻害せずに型の恩恵を十分に得ることができるため、Rubyに型を導入するメリットは大きいと思いました。その一方、実際のアプリケーションに導入するには、ライブラリの型情報の取得や型を書く労力をどう減らすかなどの課題があると思います。今後も型の導入に向けて勉強していきたいと思います。