Ateam cyma Advent Calendar 2019、20日目です!いよいよ終盤ですね!
本日は 株式会社エイチーム のねぼすけエンジニア @NamedPython がお送りします!
さて、タイトルで「お?Rubyに型?」と思ったあなた!そう、ついにRubyにも型の時代がやってこようとしているのです!
今日は、RubyKaigi2019にてStripe社から発表があったオープンソースのRuby向け型チェッカーであるSorbet🍨をふんわり紹介します!
どういう経緯・思想で作られたかは、Stripe社のプレゼン: State of Sorbet: A Type Checker for Ruby を見るととても分かりやすいです。ぜんぶ英語ですが聞き取りやすく、自動生成字幕の精度も高いので字幕つけてみるのがおすすめです。
読み方
そるべ って読みます。
カタカナで書くところの「シャーベット」なんですが初見だとわかりませんね。
パッと見
どんな感じでアノテーションつけてくのー?っていうサンプルです。公式から引用しています。
Online Playgroundもあるので是非お試しを。
def
の真上にあるsig
っていうのがアノテーションのDSLになります。
# 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: ignore
と typed: 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
ではないそうです(すげえ)
# typed: false
- Rubyの構文が正しいか
- 参照が正しいか
-
sig
が書かれているならその構文と実態をチェックする- 書かれている
sig
が矛盾していないか
- 書かれている
これが型アノテーションのないファイルのデフォルトの状態です。この状態から少しずつエラーをなくしていく(=アノテーションを付けていく)ようにすれば、漸進的に適用できるってことです。
型チェックのある世界
それでは、型チェックのある世界がどんなものかみてみましょう。セットアップもろもろは省きますが、こんなサンプルコードを用意しました。
# 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
おー、型チェック問題なしなんですね。
それならこの(危険な)メソッドが外部からアノテーションと違う呼び出され方、返し方をしたとしましょう。
例えばこんなコード:
# frozen_string_literal: true
# typed: false
require_relative 'sample'
result = Sample.typed_nillable_method("a") # 文字列を指定
result.split('').join('-')
おやおや、Symbol
とアノテーションしているのにString
を渡していますね。この状態で型チェックを行うと....?
すごーーーい!
Expected Symbol but found String("a") for argument key https://srb.help/7002
ですって。具体的で親切なメッセージですね。
今は手動で実行していますが、GitLab CI
や VSCode Extension
として仕込むことができれば、コードの安全性担保にもなりますし、生成されたRBI
をもとにIDEのメッセージも出せそうです。実際に、その例が Online Playground にはあるのでのぞいてみてください。
じゃあ次はこんなコードでどうだ!
# frozen_string_literal: true
# typed: false
require_relative 'sample'
result = Sample.typed_nillable_method(:c) # 存在しないキーを指定
result.split('').join('-')
懸念していた危険なケース、存在しないキーを参照しているようです。型チェックは....?
通っちゃいますね...。でも実行すると....?
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をご覧ください。
そのほかの職種は、エイチームグループ採用サイトをご覧ください。