Rubyのお型付け！Sorbetを試してみた

[Ateam cyma Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ateam-cyma)、20日目です！いよいよ終盤ですね！
本日は [株式会社エイチーム](https://www.a-tm.co.jp/) のねぼすけエンジニア [@NamedPython](https://qiita.com/NamedPython) がお送りします！

さて、タイトルで「お？Rubyに型？」と思ったあなた！そう、ついにRubyにも型の時代がやってこようとしているのです！
今日は、[RubyKaigi2019](https://rubykaigi.org/2019/)にて[Stripe社](https://stripe.com/jp)から発表があったオープンソースの**Ruby向け型チェッカー**である[**Sorbet**](https://sorbet.org/)🍨をふんわり紹介します！

![Sorbet logo](https://github.com/sorbet/sorbet/raw/master/docs/logo/sorbet-logo-purple-sparkles.svg?sanitize=true)

どういう経緯・思想で作られたかは、[Stripe社のプレゼン: State of Sorbet: A Type Checker for Ruby](https://youtu.be/odmlf_ezsBo) を見るととても分かりやすいです。ぜんぶ英語ですが聞き取りやすく、自動生成字幕の精度も高いので字幕つけてみるのがおすすめです。



## 読み方

そるべ って読みます。
カタカナで書くところの「シャーベット」なんですが初見だとわかりませんね。

## パッと見

どんな感じでアノテーションつけてくのー？っていうサンプルです。公式から引用しています。
[Online Playground](https://sorbet.run/)もあるので是非お試しを。

`def`の真上にある`sig`っていうのがアノテーションのDSLになります。

```ruby:sample.rb
# typed: true
class A
  extend T::Sig

  sig {params(x: Integer).returns(String)}
  def bar(x)
    x.to_s
  end
end

def main
  A.new.barr(91)   # error: Typo!
  A.new.bar("91")  # error: Type mismatch!
end
```
こちらがSorbetの型チェッカー(静的解析)の出力

```
Autocorrect: Use `-a` to autocorrect
    editor.rb:12: Replace with bar
    12 |  A.new.barr(91)   # error: Typo!
                ^^^^


editor.rb:13: Expected Integer but found String("91") for argument x https://srb.help/7002
    13 |  A.new.bar("91")  # error: Type mismatch!
          ^^^^^^^^^^^^^^^
    editor.rb:5: Method A#bar has specified x as Integer
     5 |  sig {params(x: Integer).returns(String)}
                      ^
  Got String("91") originating from:
    editor.rb:13:
    13 |  A.new.bar("91")  # error: Type mismatch!
                    ^^^^
Errors: 2
```

ふむふむ、なんだか目新しい感じですね。

## Sorbetの設計思想

僕が感じたことを書くよりも、GithubのREADMEにある[公式の設計思想: Sorbet user-facing design principles](https://github.com/sorbet/sorbet#sorbet-user-facing-design-principles) がいちばんまとまっていたので、拙いながら和訳します: 

---

**Sorbetを伴った開発は:** 

### 明確であること

型アノテーションによって、コードがより予測可能で読みやすいものになる。
このメリットが明確で、書くことに苦を感じないこと。

### 便利だし、面倒くさくない

メリットが明確なのであれば、(我々Sorbetチームは?)それをより簡潔に実現することにフォーカスする。
これらは以下のようなメリットとして表れる。

- エラーメッセージがわかりやすい
- アノテーションを充実させたぶんだけ、安全性が上がる

### 最小限だけどパワフル

別に複雑な型システムを作りたいわけじゃない。
必要な分だけ書いて、それ以外はできるだけシンプルに保ちたい。そういうシステムのほうがスケールできるし、(これは**一番重要**)使う側がSorbetを習得・理解しやすい。

### Ruby互換

新しい文法はいらない。Rubyコードであれば、ほとんどのツール(エディタ等)がそのまま利用できる。
Sorbetの強みである漸進的改善(gradually improve)を、既存のコードベースに行えるようにする。

### スケールする

実行速度、開発チームの人数、コード行数、コードの築年数、どんな軸でもスケールする。
僕たちは実際に巨大なRubyコードベースで仕事をしているし、大きくなる一方だから。

### 漸進的に適用できる

スケールする中で(Sorbetを)適用するには、プロジェクトやチームに一気に導入しなければいけない。
つまり、Sorbetは段階的に、プロジェクトに合ったスピードで適用できる必要がある。

---

ということでした。

使ってみた感想でいうと、

- **簡単に習得できる**
- **漸進的に適用できる**

が重要なポイントなように思います。


## 漸進的に適用できるってどゆこと

例えばですが、漸進的**ではない**適用を考えてみると

- 型アノテーションを付けない限り実行を許さなくなる
- ひたすら型アノテーションがついていない箇所を列挙され、コードベースが大きければ大きいほどエラーメッセージが出続ける

のようなものが考えられますね。
しかしSorbetでは、セットアップの段階で段階的に適用できるようにマジックコメントが用意してあります。

### # typed:

`# typed: ` は1ファイルごとにつけるものです。
`# frozen_string_literal: true` みたいなものですね。

以下の5種類があります。



`typed: ignore` | `typed: false` | `typed: true`  | `typed: strict` | `typed: strong` |
|--- | --- | --- | --- | --- | 
| 解析をしないし、エラーを出さない | 解析し、アノテーションがある部分のみエラーを出す | 解析し、型を認識できない場合はエラーを出す | エラーを出す上に、型アノテーションを強制する | すべての型チェックエラーを出す |

`typed: ignore` と `typed: false`がありますが、この違いが結構大事です。今回はこのふたつのみ詳細を書きます。

#### # typed: ignore

もはやSorbetが解析すらしてくれません。なので、もし`# typed: ignore`なRubyソースを別のソースから参照していると、参照先でエラーが出ることになります。
なので、[公式ドキュメント](https://sorbet.org/docs/static)でも可能な限り`# typed: ignore`なファイルをなくしていくよう推奨しています。
> We recommend pushing the entire project to out of ignore (at Stripe, 100% of non-test files are not ignored.)

Stripe社では、テストコード以外はすべて`typed: ignore`ではないそうです(すげえ:rolling_eyes:)

#### # typed: false

- Rubyの構文が正しいか
- 参照が正しいか
- `sig`が書かれているならその構文と実態をチェックする
    - 書かれている`sig`が矛盾していないか

これが型アノテーションのないファイルのデフォルトの状態です。この状態から少しずつエラーをなくしていく(=アノテーションを付けていく)ようにすれば、**漸進的に適用できる**ってことです。

## 型チェックのある世界

それでは、型チェックのある世界がどんなものかみてみましょう。セットアップもろもろは省きますが、こんなサンプルコードを用意しました。

```ruby:sample.rb
# typed: true

require 'sorbet-runtime'

class Sample
  extend T::Sig

  FIXED_HASH = {
    a: 'alpha',
    b: 'beta'
  }.freeze

  sig { params(key: Symbol).returns(String) }
  def self.typed_nillable_method(key)
    FIXED_HASH[key]
  end
end
```

なんだか危険なコードですね...
`FIXED_HASH` にないキーでアクセスすると`nil`を返すのですが、`Sample#typed_nillable_method`で一切制御されていません。

でも、

- `# typed: true`
- `sig`によるアノテーション

ができています。

この状態で型チェックをやってみます `# => bundle exec srb tc`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/449bef49-d7a6-d7ab-0c24-277ff50cbcc6.png)
おー、型チェック**問題なし**なんですね。

それならこの(危険な)メソッドが外部からアノテーションと違う呼び出され方、返し方をしたとしましょう。
例えばこんなコード:

```ruby:unsafe_code.rb
# frozen_string_literal: true
# typed: false

require_relative 'sample'

result = Sample.typed_nillable_method("a") # 文字列を指定

result.split('').join('-')
```

おやおや、`Symbol`とアノテーションしているのに`String`を渡していますね。この状態で型チェックを行うと....?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/c88b959b-5511-15b0-dfb5-5317708c905a.png)
**すごーーーい！**
`Expected Symbol but found String("a") for argument key https://srb.help/7002` ですって。具体的で親切なメッセージですね。

今は手動で実行していますが、`GitLab CI` や `VSCode Extension` として仕込むことができれば、コードの安全性担保にもなりますし、生成された`RBI`をもとにIDEのメッセージも出せそうです。実際に、その例が [Online Playground](https://sorbet.run/) にはあるのでのぞいてみてください。

じゃあ次はこんなコードでどうだ！

```ruby:unsafe_code.rb
# frozen_string_literal: true
# typed: false

require_relative 'sample'

result = Sample.typed_nillable_method(:c) # 存在しないキーを指定

result.split('').join('-')
```
懸念していた危険なケース、存在しないキーを参照しているようです。型チェックは....?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/b7810e14-857d-1f21-83d4-d9281b1df147.png)
通っちゃいますね...。でも実行すると....?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/e92e9d1d-db9d-d765-01bc-8a08e739bc8a.png)

```
Return value: Expected type String, got type NilClass (TypeError)
Caller: unsafe_code.rb:6
Definition: .../sample.rb:14
```

おお、通常吐かれる`TypeError`をさらにラップして、該当の部分を詳細に示してくれています。
静的解析もできるし、実行時にもチェックしてくれる....**最高か**....


## まとめ

型のあるRuby、`Sorbet`いかがでしたでしょうか。情報収集が十分ではないので名前のみにとどめますが、`Sorbet`の他にも [Soutaro Matsumoto](https://twitter.com/soutaro) さん作の [Steep](https://github.com/soutaro/steep) というものもあります。

どちらも素敵な思想をもっているので、ぜひ試してみてください。

## おわりに

[Ateam cyma Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ateam-cyma)、20日目いかがでしたか？
21日目は 早起きエンジニア [@shimura_atsushi](https://qiita.com/shimura_atsushi) が [前回](https://qiita.com/shimura_atsushi/items/cb29b1d3841fc21d460b) の続きでOCRに挑む 記事を書かれるそうですので、お楽しみに！

株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。

エンジニアとしての働き方に興味を持たれた方は[cymaのQiita Jobs](http://bit.ly/35E1I08)をご覧ください。

そのほかの職種は、[エイチームグループ採用サイト](http://bit.ly/2XQpd3q)をご覧ください。