Rubyの型を宣言するRBSについて調べてみた。

こんにちは! バックエンドチームの久野です!

自分は、Rubyを業務で使用していますが、動的型付け言語でも静的型検査の仕組みを導入する動きが近年活発になっているので、Rubyでも型を宣言することを可能にするRBSについて調べてみました。

まずはRBSとは何かについて調べてみました。

RBSとは、Rubyの静的型付けに対応するために開発された形式的なインターフェースの一つで、RBSはRuby3.0で導入され、Rubyプログラマーによる型アノテーションの作成を容易にし、型に関する情報を共有することを可能する言語です。

上記の説明で出てきたアノテーションという単語に馴染みが無かったのでさらに調べてみました。

アノテーションとは、プログラミング言語において、変数や関数、メソッドなどの要素に型情報を付与することを指します。

簡単にまとめると、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メソッドに引数abが渡されていますが、これらの引数の型は明示していません。そのため、引数に誤った型が渡された場合には、実行時にエラーが発生してしまいます。
このようなケースの問題を解決するために、アノテーションを使用することが有効になりそうですね。

今度は、実際にアノテーションを使用して、以下のようにコードを書きかえてみました。

# 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)

こちらのコードでは、引数abの型をIntegerと明示しています。また、戻り値の型もIntegerと明示しています。
これで、上記のコードには間違いがあると分かりやすくなりましたね。今回のケースでは、アノテーションを使用することで、コードの読みやすさや保守性が向上し、バグの発生確率を低減することができるようになりました。

ただし、アノテーションを使用する場合でも、Rubyは動的型付け言語であるため、実行時にオブジェクトの型が変わる可能性があるそうで。
静的型付け言語での型情報とは異なり、あくまで予想される型を表すものである程度であることに注意が必要だとわかりました。


次に、RBSとセットで使われることが多い、TypeProSteepも試してみました。具体的には、以下の流れのように使われることが多いそうです。

  1. コードを用意する。

  2. TypeProfアノテーションの形式にコードを型推論させ出力させる。

  3. 出力させたコードをRBSファイルへと実際に置き換える。

  4. 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について調べたことで、動的言語であっても静的型付けのトレンドを感じ、アップデートさせ続ける必要性があるのだと分かりました。

動的言語、静的言語のどちらも使ってみることで、
お互いの特性を把握し適切に使い分けられるようにしていくことが重要そうですね。


この記事が気に入ったらサポートをしてみませんか?