まさひlog

趣味で行っていることをのんびりとログとして残していきます

Rubyに静的型付けSorbetを触ってみる

ついにRubyでも静的型付けができるようになりましたね。

Typescriptなど少し前から、静的型付けが熱いですがやっとRubyでも!
僕自身、業務上ではRubyを使っていて、動的型付けに悩まされることもあるので嬉しいです!

とはいえ、触ってみないとわからないため、今回はそのメモです。

今回はSorbetのPlaygroundで簡単に触ってみます。

sorbet.run

Sorbetとは?

  • Stripeが作成
  • Ruby用に設計された強力な型チェッカーライブラリ
  • IDEに対応
  • Gradual Type Checking

Gradual Type Checkingは段階的な型チェック
つまり、ファイル単位やメソッド単位など細かい粒度で採用していけるということ。

「Sorbetを採用したいけどシステムが大規模...」
「どれだけの時間がかかるか...」

といったことがなく、1つ1つのファイル対応や重要な処理を優先的にできる!
もちろん全て対応できた方がいいと思います...

メソッドシグネチャ

コード内で型チェックを可能にするための主な方法

# typed: true
require 'sorbet-runtime'

class Sample
  # シグネチャを使用するため、クラス・モジュールのトップに必要
  extend T::Sig

  sig {params(x: Integer, y: Integer).returns(Integer)}
  def self.sum(x, y)
    x + y
  end
end

Sample.sum(1, 2) # => 3
Sample.sum('aa', 'bb') # => error
sig {params(x: Integer, y: Integer).returns(Integer)}

これは、引数xとyがInteger型、戻り値がIntegerという意味です。
以下のように複数行でも可能なため、長くなりそうな時でも可読性は問題なさそうです。

sig do
  params(
    x: Integer,
    y: Integer
  )
  .returns(Integer)
end

戻り値がない場合は

  sig {void}
  def self.print_bar
    puts 'bar'
  end

とかけます

アサーション

種類としては4つあります。

  • T.let(expo, Type)
  • T.cast(expo, Type)
  • T.must(expo)
  • T.assert_type!(express, Type)

T.let

変数代入で型指定ができる

x = T.let(10, Integer)

y = T.let(10, String) => error

T.cast

# typed: true
require 'sorbet-runtime'

extend T::Sig

class A; def foo; end; end
class B; def bar; end; end

sig {params(label: String, a_or_b: T.any(A, B)).void}
def foo(label, a_or_b)
  case label
  when 'a'
    a_or_b.foo #  Not enough arguments provided for method Object#foo on B component of T.any(A, B).
  when 'b'
    a_or_b.bar #   Method bar does not exist on A component of T.any(A, B)
  end
end

これは、引数にそんなメソッドはないと言われています。 そのため以下のようにcastしてclassを明示してあげる必要があります。

 case label
  when 'a'
    T.cast(a_or_b, A).foo
  when 'b'
    T.cast(a_or_b, B).bar
  end

T.must

変数の中身がnilだとエラーを出す。

使われていない変数の検知やnilが入って欲しくない変数に使えそう?

class A
  extend T::Sig

  sig {void}
  def foo
    x = T.let(nil, T.nilable(String))
    y = T.must(nil)
    puts y # error: This code is unreachable
  end

  sig {void}
  def bar
    vals = T.let([], T::Array[Integer])
    x = vals.find {|a| a > 0}
    T.reveal_type(x) # Revealed type: T.nilable(Integer)
    y = T.must(x)
    puts y # no static error
  end

end

T.assert_type!

以下では、assert_typeでxはString型と断言しているが、xは型がないためエラーになっています。

class A
  extend T::Sig

  sig {params(x: T.untyped).void}
  def foo(x)
    T.assert_type!(x, String) # error here
  end
end

まとめ

今回はPlaygroundを使って、どんな感じなのかを触ってみました。
まだ触り始めたばかりですが、型があることで変数に何が入ってくるのかわかりいいですね。
あと段階的に型に対応していけるのは嬉しいですね!

参考

Adopting Sorbet in an Existing Codebase · Sorbet