Rubyの型を宣言するRBSについて調べてみた。
こんにちは! バックエンドチームの久野です!
自分は、Rubyを業務で使用していますが、動的型付け言語でも静的型検査の仕組みを導入する動きが近年活発になっているので、Rubyでも型を宣言することを可能にするRBSについて調べてみました。
まずはRBSとは何かについて調べてみました。
上記の説明で出てきたアノテーションという単語に馴染みが無かったのでさらに調べてみました。
簡単にまとめると、RBSは、Rubyでアノテーションを可能にする言語だという事になりそうですね。
より理解を深めるため、
サンプルとして以下のようなRubyのコードを用意してみました。
# sample.rb
def add(a, b)
return a + b
end
puts add(1, "2") # TypeError (String can\'t be coerced into Integer)
こちらのコードでは、addメソッドに引数aとbが渡されていますが、これらの引数の型は明示していません。そのため、引数に誤った型が渡された場合には、実行時にエラーが発生してしまいます。
このようなケースの問題を解決するために、アノテーションを使用することが有効になりそうですね。
今度は、実際にアノテーションを使用して、以下のようにコードを書きかえてみました。
# sample.rbs
def add(a: Integer, b: Integer) -> Integer
return a + b
end
puts add(1, "2") # TypeError (String can\'t be coerced into Integer)
こちらのコードでは、引数aとbの型をIntegerと明示しています。また、戻り値の型もIntegerと明示しています。
これで、上記のコードには間違いがあると分かりやすくなりましたね。今回のケースでは、アノテーションを使用することで、コードの読みやすさや保守性が向上し、バグの発生確率を低減することができるようになりました。
ただし、アノテーションを使用する場合でも、Rubyは動的型付け言語であるため、実行時にオブジェクトの型が変わる可能性があるそうで。
静的型付け言語での型情報とは異なり、あくまで予想される型を表すものである程度であることに注意が必要だとわかりました。
次に、RBSとセットで使われることが多い、TypePro、Steepも試してみました。具体的には、以下の流れのように使われることが多いそうです。
コードを用意する。
TypeProfでアノテーションの形式にコードを型推論させ出力させる。
出力させたコードをRBSファイルへと実際に置き換える。
Steepで型エラーを検出する。
上記の検証のため、gemは下記の3つを使いました。
gem "rbs"
gem "typeprof"
gem "steep"
実際にこちらの用意したコードで、1〜4までの流れを検証していきます。
# lib/information.rb
class Information
def initialize(animal:, height:)
@animal, @height = animal, height
end
end
こちらのコードに対して、typeprofコマンドを実行して型を推論させます。
$ bundle exec typeprof lib/information.rb
コマンド実行後、型推論されたコードがターミナルに出力されました。
# Classes
class Information
@animal: untyped
@height: untyped
def initialize: (untyped, untyped) -> [untyped, untyped]
end
コマンド実行前より、アノテーションの形式に近づきましたね。
しかし、まだ具体的な型が明記されていないので、型を追記してあげます。
#sig/information.rbs
class Information
attr_reader animal : String
attr_reader height : Integer
def initialize : (animal: String, height: Integer) -> nil
end
これでRBSファイルが完成しました。
次に、用意したRBSファイルのメソッドを実際に使用するコードを作成しました。こちらのコードでは、あえて変数heightの型が間違っている状態にしました。Steepの型エラーの検出が正しく働けば、型エラーを検知できる想定です。
# main.rb
require "./lib/information"
def main
info = Information.new(animal: "Fukurou", height: "80")
puts info.animal
end
それでは、Steepコマンドを実行し型エラーを検出してみます。
$ bundle exec steep check
main.rb:4:52: IncompatibleAssignment: lhs_type=::Integer, rhs_type=::String ("80")
::String <: ::Integer
::Object <: ::Integer
::BasicObject <: ::Integer
==> ::BasicObject <: ::Integer does not hold
型が異なるため、ターミナルにエラー分が表示され、
エラーを正しく検知することができました!
型の不整合でエラーが発生してしまうこともプロダクトでは度々あるため、事前にエラー検知できる仕組みはありがたいですね。そして、なぜ静的型言語が保守性が高いのか、型のエラー検知を実際に試してみて、身近に感じることができました。
今回、RBSについて調べたことで、動的言語であっても静的型付けのトレンドを感じ、アップデートさせ続ける必要性があるのだと分かりました。
動的言語、静的言語のどちらも使ってみることで、
お互いの特性を把握し適切に使い分けられるようにしていくことが重要そうですね。
この記事が気に入ったらサポートをしてみませんか?