Sorbetのruntimeチェックを試してみた

SorbetはRubyのシンタックスで書かれているため、型アノテーションの情報を実行時にも保持できます。
(Steepはコメントアウトで型アノテーションを記述するため、実行時には型の情報が捨てられてしまっているようです)

これを用いて、実行時に型アノテーションの検査ができるというので、手元で試してみました。

https://sorbet.org/docs/runtime

## Sorbet Runtimeについて

Sorbetは部分的に型チェックを有効化、無効化することが可能です。そのため、型アノテーションを記述しても異なる型の値が渡される可能性があります。

Sorbet Runtimeは実行時に型情報を用いて、アノテーションをチェックすることができます。

これにより、型アノテーションの誤りに気づくことができます。

## 試してみる

まずは型チェックを無効化してプログラムを実行してみます。

```rb:untyped.rb
# typed: false

class Untyped
  attr_reader :age

  def initialize(age:)
    @age = age
  end

  def string_age
    age.to_s
  end
end
```

```rb:rspec
require 'rails_helper'

RSpec.describe Untyped do
  describe '#string_age' do
    subject { described_class.new(age: age).string_age }

    context 'when age is Integer' do
      let(:age) { 20 }

      it { is_expected.to eq('20') }
    end

    context 'when age is not Integer' do
      let(:age) { nil }

      it { is_expected.to eq('') }
    end
  end
end
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/30b902cc-70ba-474c-49a3-4b007e4cf6c3.png)

エラーが起きていないことが確認できました。

### 型チェックを有効化

次に型アノテーションを記述して、型チェックを有効化します

```rb:typed.rb
# typed: strict

class Typed
  extend T::Sig

  sig { returns(Integer) }
  attr_reader :age

  sig { params(age: Integer).void }
  def initialize(age:)
    @age = T.let(age, Integer)
  end

  sig { returns(String) }
  def string_age
    age.to_s
  end
end
```

するとエラーが発生しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/eec77de7-8da0-3cd5-60e8-7000ae1df343.png)

ageにIntegerを期待していたのにNilClassを受け取ったことでエラーになっています。そしてTypeErrorがraiseされています。

### ランタイムチェックを無効化したいとき

デフォルトではすべての環境においてランタイムチェックが実行されます。

これを変更したり、無効化したりするには設定や環境変数が必要です。

https://sorbet.org/docs/runtime#checked-whether-to-check-in-the-first-place

```rb
T::Configuration.default_checked_level = :tests # default :always
```

環境変数の場合は `SORBET_RUNTIME_DEFAULT_CHECKED_LEVEL` を使います。

以下のようにアノテーションされているコードに対して、 `SORBET_RUNTIME_DEFAULT_CHECKED_LEVEL=never` で実行します。

```rb
# typed: true

class Typed
  extend T::Sig

  sig { returns(Integer) }
  attr_reader :age

  sig { params(age: Integer).void }
  def initialize(age:)
    @age = age
  end

  sig { returns(String) }
  def string_age
    age.to_s
  end
end
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/21eac420-e478-5e77-0c4f-ef13b685ab15.png)

このようにエラーが発生しませんでした。

しかし、`T.let`, `T.cast`, `T.must`, `T.bind`といった型アサーションを使った場合、必ずチェックされるので注意が必要です。

https://sorbet.org/docs/type-assertions

```diff_ruby
# typed: strict

class Typed
  extend T::Sig

  sig { returns(Integer).checked(:never) }
  attr_reader :age

  sig { params(age: Integer).void.checked(:never) }
  def initialize(age:)
+   @age = T.let(age, Integer)
  end

  sig { returns(String).checked(:never) }
  def string_age
    age.to_s
  end
end
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/2e619621-de85-f87b-4d7b-f58d29ed7277.png)

このように型アサーションは常にRubyコードとして実行されるため、わずかながらオーバーヘッドがかかるようです。そのため、ドキュメントにも型アサーションを使わず、メソッドシグニチャを使う方法も紹介されています。

https://sorbet.org/docs/type-assertions#prefer-method-signatures-over-type-assertions

## 終わりに

RSpec等を実行したときに型アノテーションのチェックができるというのはとてもいいと思いました。
YARDからRBIを生成するツールもあるので、それで生成した後にSorbet Runtimeでチェックするとかを今度試してみようと思いました。