LoginSignup
0

More than 3 years have passed since last update.

posted at

updated at

Organization

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

Ateam cyma Advent Calendar 2019、20日目です!いよいよ終盤ですね!
本日は 株式会社エイチーム のねぼすけエンジニア @NamedPython がお送りします!

さて、タイトルで「お?Rubyに型?」と思ったあなた!そう、ついにRubyにも型の時代がやってこようとしているのです!
今日は、RubyKaigi2019にてStripe社から発表があったオープンソースのRuby向け型チェッカーであるSorbet🍨をふんわり紹介します!

Sorbet logo

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

読み方

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

パッと見

どんな感じでアノテーションつけてくのー?っていうサンプルです。公式から引用しています。
Online Playgroundもあるので是非お試しを。

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

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 がいちばんまとまっていたので、拙いながら和訳します:


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: ignoretyped: falseがありますが、この違いが結構大事です。今回はこのふたつのみ詳細を書きます。

# typed: ignore

もはやSorbetが解析すらしてくれません。なので、もし# typed: ignoreなRubyソースを別のソースから参照していると、参照先でエラーが出ることになります。
なので、公式ドキュメントでも可能な限り# 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が矛盾していないか

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

型チェックのある世界

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

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
おー、型チェック問題なしなんですね。

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

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
すごーーーい!
Expected Symbol but found String("a") for argument key https://srb.help/7002 ですって。具体的で親切なメッセージですね。

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

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

unsafe_code.rb
# frozen_string_literal: true
# typed: false

require_relative 'sample'

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

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

懸念していた危険なケース、存在しないキーを参照しているようです。型チェックは....?
image.png
通っちゃいますね...。でも実行すると....?
image.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 さん作の Steep というものもあります。

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

おわりに

Ateam cyma Advent Calendar 2019、20日目いかがでしたか?
21日目は 早起きエンジニア @shimura_atsushi前回 の続きでOCRに挑む 記事を書かれるそうですので、お楽しみに!

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

エンジニアとしての働き方に興味を持たれた方はcymaのQiita Jobsをご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0