Sorbet の型定義どこで書くのかとか、 Tapioca の作ってくれる型定義ファイル何なのかとかよく混乱しがちなので、整理しました。
Sorbet の型を書く方法
1. Ruby コード (.rb
) 内に Sorbet の記法を使って書く
自分のコードに対して、型を書きたい場合は、基本的に以下のように、コード中に Sorbet の記法 (sig
や T.let
など) を使って書きます。
# typed: true
require 'sorbet-runtime'
class Doubler
extend T::Sig
sig { returns(Integer) }
attr_reader :value
sig { params(value: Integer).void }
def initialize(value)
@value = T.let(value, Integer)
end
sig { returns(Integer) }
def doubled
@value * 2
end
end
Klass.new(4).doubled
自分のコードに、処理のコードと、型のコードが入り交じることになるのですが、この方法で書いた場合、ランタイムチェック (プログラムの実行中の値が、定義した型に合っているかどうかを、実行中に都度チェックする) も行うことができます。 (ある種のアサーションとしての機能も果たしてくれます)
2. RBI ファイル (.rbi
) に書く
- DSL などのメタプログラミングをしていて、コード内で Sorbet の記法を使って型を書くのが難しい場合
- gem など外部のコードに対して、型を記述したい場合
- (gem を書いているときに、
sorbet-runtime
への依存をしたくない場合)
には RBI ファイルを使って型を書く、あるいは RBI ファイルを生成することになります。
RBI ファイルとは
RBI ファイルは、rbs で言う .rbs
ファイル、 TypeScript でいう .d.ts
ファイルのような型定義ファイルです。 srb
コマンド による静的型検査で使われます。 ランタイムでは読み込まれないため、 RBI ファイルがプログラムの実行結果に影響を及ぼすことはありません。
# typed: true
class Klass
sig { returns(Integer) }
attr_reader :value
sig { params(value: Integer).void }
def initialize(value); end
sig { returns(Integer) }
def doubled; end
end
RBI ファイルの特徴としては、文法自体は Ruby ファイルと変わらない という特徴があります。 Ruby ファイルと書くときは、以下の違いがある程度で、基本的には殆ど同じです。
- method の body など、インターフェイスを定義するのに不要な要素は書いてはいけない
-
extend T::Sig
,require 'sorbet-runtime'
のような 「Sorbet の記法を使えるようにするための各種宣言」が不要- ※ ファイル先頭の、
# typed: XXX
の宣言は必要になります。
- ※ ファイル先頭の、
(文法が同じということは、 Ruby のシンタックスハイライト、 Parser、 Linter 等が使い回せる、ということなので、エコシステムが整えられやすいという大きなメリットがあるなと感じます )
※ RBI ファイルで書いた方のランタイムチェックは行われない
Ruby コード内に型を書く場合と違い、 RBI ファイルで書いた型に対してはランタイムチェックは行われません。
RBI ファイルはプログラムの実行結果に影響を及ぼすことはない、と書きましたが、これはランタイムチェックについても同様、です。ランタイムチェックをしたい場合は Ruby コード内に直接型を書く、ということになりmす。
RBI ファイルの主な置き場所
RBI ファイルの置き場自体は、 srb
コマンドで指定しさえすれば任意の場所に置けるので自由です。(標準的には srb .
で、カレントディレクトリと、その子孫ディレクトリを全部探索するので、そのどこかに置けば OK です。)
とはいえ、 Sorbet と Tapioca 側で、慣習的な置き場所は用意されています。(tapioca init
すると、これに沿って置き場所を用意してくれます)
RBI ファイルの生成や管理は Tapioca に頼ることになるので、基本はこれに従っておくのが無難でしょう。
以降は、慣習的に何をどこに置くのかを紹介していきます。
sorbet/rbi/annotations
: コミュニティで書かれた gem の型定義を置く場所
Ref: https://github.com/Shopify/tapioca#pulling-rbi-annotations-from-remote-sources
Shopify/rbi-central という Tapioca 版 DefinitelyTyped が存在し、各種 gem に対応した RBI ファイルがコミュニティによって書かれ、メンテナンスされています。
Tapioca が、そこから使っている gem の型定義をダウンロードして、 sorbet/rbi/annotations
ディレクトリに配置してくれます。
sorbet/rbi/gem
: Tapioca が gem を解析して生成した型定義を置く場所
Ref: https://github.com/Shopify/tapioca#generating-rbi-files-for-gems
sorbet/rbi/annotations
とは別の、 gem 用の rbi ディレクトリとして sorbet/rbi/gem
があります。こちらはコミュニティが書いたものではなく、Tapioca がその場で gem を機械的に解析した結果をもとに生成した型定義が置かれています。
具体的に、どのような解析をしているかを説明するのはかなり難しいので、詳細は割愛しますが、大体以下の情報をもとに生成を行っています。
- gem を実際に読み込んで (
require
して) 得られたメソッド等の情報 - gem のソースコード内の、コメント、Sorbet の記法等の情報
- gem が用意している RBI ファイル (
rbi/
ディレクトリ内) の内容
実際に生成される RBI ファイルはこんな感じになります。
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `parser` gem.
# Please instead update this file by running `bin/tapioca gem parser`.
# {Parser::AST::Node} contains information about a single AST node and its
# child nodes. It extends the basic [AST::Node](http://rdoc.info/gems/ast/AST/Node)
# class provided by gem [ast](http://rdoc.info/gems/ast).
#
# @api public
#
# source://parser//lib/parser/ast/node.rb#17
class Parser::AST::Node < ::AST::Node
# Assigns various properties to this AST node. Currently only the
# location can be set.
#
# @api public
# @option properties
# @param properties [Hash]
#
# source://parser//lib/parser/ast/node.rb#30
def assign_properties(properties); end
# Source map for this Node.
#
# @api public
# @return [Parser::Source::Map]
#
# source://parser//lib/parser/ast/node.rb#18
def loc; end
様々な解析を駆使しているので、保持しているメソッド等の情報はかなり正確なのですが、 (gem 側で Sorbet の型を書いていない限りは) ほとんどのメソッドの型は untyped になっています。そのため、型検査したい部分については、自分で gem の各メソッドの型を書くことになります。
※ gem の解析時に require するファイルの指定が必要なケースもある (sorbet/tapioca/require.rb
で指定する)
Ref: https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem
Tapioca が gem を解析する際に require
して実際に読み込んでいる、と書きましたが、どのファイルを require するかは Tapioca が自動で推定しているので、解析に必要なファイルを読み込めていない場合が偶にあります。
(require: false
と Gemfile.lock
内で指定している場合や、 ActiveSupport のように、 require するファイルで使える機能が変わるケースなど)
その場合には、 Tapioca に sorbet/tapioca/require.rb
で、どのファイルを require するべきかを指示する必要があります。
(※ sorbet/tapioca/require.rb
は Ruby コードではありますが、あくまで解析用でプログラムの実行時には影響はありません)
sorbet/rbi/dsl
: DSL などのメタプログラミングをしているコードの型定義を生成して置く場所
Ref: https://github.com/Shopify/tapioca#generating-rbi-files-for-rails-and-other-dsls
Ref: Tapioca の DSL compiler のしくみ - Qiita
DSL などのメタプログラミングをしているコードに対して型を与えたい場合は RBI ファイルを書く必要があるのですが、楽をするためのメタプログラミングなのに、型は手動で書かないといけないというのは本末転倒感がありつらいですね…。
なので、DSL に対応した RBI ファイルの生成を行えるようにする、 DSL compiler という仕組みが Tapioca では用意されています。これで生成した RBI ファイルを sorbet/tapioca/dsl
に置きます。
Rails などの主要なライブラリの DSL 用の DSL compiler は Tapioca 内に同梱されているほか、自分で DSL compiler を書いたり、 gem にそれ用の DSL compiler を同梱させることが可能です。
詳しくは Tapioca の DSL compiler のしくみ - Qiita にまとめているので、そちらを参照ください。
sorbet/rbi/shims
: 自分で書いた RBI ファイル (shim) を置く場所
Ref: [Ruby][Sorbet] 自分で書いたRBIをどこに置くべきか
Ref: https://sorbet.org/docs/rbi#hand-written-rbis-for-gems
Ref: https://github.com/Shopify/tapioca#manually-writing-rbi-definitions-shims
自分で書いた RBI ファイル (shim と呼ばれています) の置き場所としては、 sorbet/rbi/shims
があります。その中に置き方として、以下の方法が紹介されているので参考にすると良いでしょう。
- Gem の型定義:
sorbet/rbi/shims/gems
内に置く。命名規則はsorbet/rbi/gem
内に近づける。 - 自分のコードの型定義: 自分の Ruby ファイルの位置と対応するように RBI ファイルを置く。
- 例:
app/models/person.rb
の型をsorbet/rbi/shims/app/models/person.rbi
に書く。 - Ref: https://github.com/Shopify/tapioca#rbi-files-for-missing-constants-and-methods
- 例:
sorbet/rbi/todo.rbi
: Sorbet が見つけられていない定数の仮定義が生成される場所
Ref: https://sorbet.org/docs/rbi#the-todo-rbi-file
tapioca todo
によって生成される RBI ファイルです。
「Ruby コード内では参照されているが、 Sorbet からは定義を見つけられていない定数」に対して、 Sorbet のエラーを回避するために、仮の定義がここに書かれます。仮の定義なので、何かしら別の RBI ファイルで定義するようにして、ここは空になるのが望ましいです。