📖

Rubyの型チェック入門 - Sorbetとtapiocaの基本知識

2024/01/30に公開

こんにちは。
PharmaX でエンジニアをしている諸岡(@hakoten)です。

この記事の概要

この記事では、Rubyの型チェックシステムである、Sorbetとtapiocaについて、基本的な知識を紹介しています。

PharmaXのバックエンドではRuby on Railsを使用しており、型チェックツールとしてStripe社が主導するSorbetを採用しています。また、Sorbetの型を生成するために、Shopify社が開発したtapiocaも一緒に利用しています。

私はRailsの経験はありますが、型チェックを取り入れたプロジェクトへの参加経験はあまりありませんでした。しかし最近、Sorbetの運用タスクに携わる機会があり、Sorbetとtapiocaの基本を調べる機会がありましたので、その内容を記事にまとめました。

本記事では、Sorbetの具体的な導入方法や型の定義の仕方には触れていません。詳細については、公式ドキュメントや、少し古いですが弊社のエンジニアである加藤(@tomo_k09)が書いた記事を参照していただければと思います。

https://sorbet.org/
https://github.com/Shopify/tapioca
https://note.com/pharmax/n/ncad2a5a8cb88

Rubyの型定義について

Sorbetの話をする前に、まずはRuby言語の型定義について簡単に整理します。
Rubyの型定義には、大きく分けて「RBI(Ruby Interface)」「RBS(Ruby Signature)」の2つが存在します。

型定義 説明
RBI(Ruby Interface) Stripeが開発した独自の型定義言語です。今回の記事にあるSorbetで利用されています。
RBS(Ruby Signature) Rubyプログラムの構造を記述するための言語です。ruby 3.0より、言語システムとして正式採用されました。

RBSは今回の主な対象ではありませんので、詳細は割愛しますが、RBSは「TypeProf」という型解析ツールと一緒にRubyのツールチェインとして内包されています。
また、RBSを使ったTypeProfとは別の型解析ツールとして「steep」があります。

TypeProfとsteepの主な違いは、型注釈(実際のRubyコード上に書く型の注釈)を書けるかどうかで、steepはsorbetと同様に型注釈が使用できます。

ざっくり一覧にすると以下のようになります。

解析ツール 型定義のフォーマット 型注釈を使った解析
TypeProf RBS
steep RBS
Sorbet RBI

SorbetはRBS → RBIへの変換開発なども行っていますが、主に使われるRBIはRBSとは別のフォーマットであることに注意してください。

Sorbet と tapiocaの関係

Sorbetは、前述の通り、「RBIで作成された型定義ファイルを使って型の解析を行うツール」です。

一方、tapiocaは、「解析で使われるRBIファイルを生成するためのツール」であり、tapiocaを使うとアプリケーションで使用されているgemやDSLで定義された型情報をRBIファイルとして出力してくれます。

tapiocaは、Sorbetのインストールガイドにもtapiocaがお勧めされているくらいの主要なツールです。
少し前まではsorbet-railsやSorbetのCLIを使った型生成を行っていましたが、現在はTapiocaに統一するような流れになっているようです。


Sorbetのインストールガイドでもtapiocaが推奨されています。

https://sorbet.org/docs/adopting

Sorbetの型チェックの概要

ここからは、Sorbetでは、どのように型のチェックを行うのかを簡単に記載します。
Sorbetでは大きく、「静的チェック」「動的(Runtime)チェック」の2つの型チェック機能を提供しています。

Sorbetの静的チェック

Sorbetでは、srb というコマンドを使って静的解析による型チェックを行うことができます。

(型チェックのコマンド)

bundle exec srb tc

静的チェックを行うには、実装したファイルの先頭に「sigils」と呼ばれる宣言を記載する必要があります。

(sigilsの宣言例)

# typed: true

sigilsでは、「どのくらい厳格に型チェックをするか」というレベルを宣言することができます。

厳格さのレベル 説明
ignore ファイルはSorbetによって読み込まれず、そのファイル内でのエラーは一切報告されません。
false 構文エラーや定数の解決、sigの正確さに関連するエラーのみが報告されます。
true 存在しないメソッドの呼び出しや引数の数の不一致、変数の型との不整合などの「型エラー」がチェックされます。
strict すべてのメソッドにシグネチャの記述が必要で、すべての定数とインスタンス変数には明示的に注釈された型が必要です。
strong T.untypedを使用してはいけません。

ポイントは、「falseであっても型やシグネチャの解析は行われる点」、「ignoreはファイルの読み込み自体が行われない(型やシグネチャが読み込まれない)点」です。

複数のファイル間で参照される定数などが宣言されているRubyファイルがignoreに設定されていると、型情報が見つからず srb tc でエラーになるため注意が必要です。

運用では、本当に影響がないファイルのみ「ignore」を使い、型注釈の難易度が高いファイルであっても最低限「false」を付けておくのが良いかと思います。

sig(型注釈)の書き方

ここでは、静的型チェックを行う場合の型注釈の書き方を簡単に記載します。

# NOTE: sigilsによるレベルの宣言

# typed: true

# NOTE: 型注釈を書くには「T::Sig」をextendする必要があります。
extend T::Sig

# NOTE: 型注釈
# NOTE: paramsで引数の型を宣言し、.voidは戻り値の型を示します。
# NOTE: 戻り値がある場合は、「.returns(<戻り値の型>)」で宣言できます。
sig {params(env: T::Hash[Symbol, T.untyped], key: Symbol).void}
def log_env(env, key)
  puts "LOG: #{key} => #{env[key]}"
end

log_env({timeout_len: 2000, user: 'jez'}, :user)

型チェックには、次の2つの記載が必要です。

  • 「extend T::Sig」
  • sigから始まる型注釈

その他、静的型チェックの詳細については、以下の公式ページを参照してください。

https://sorbet.org/docs/static

sigの詳細な書き方については、「Type System」を含む以下の公式ページに詳しく記載されています。

https://sorbet.org/docs/sigs

Sorbetの動的(Runtime)チェック

Sorbetでは sorbet-runtime というgemを別途インストールすることでコード実行時に型のチェックを行うことができます。

静的チェックに加えて、更にRuntimeチェックを行うことで、「型注釈が間違っていることを実行時に気づくことができる」というメリットがあります。

具体的には、メソッド単位で型注釈が正しい場合には、静的チェックには通りますが、実際に実行すると意図されない値が渡される場合が考えられます。

Runtimeチェックを行っていると、実行時に渡された型と型注釈で書いた型が不一致の場合にSorbetのエラーをスローするため、型注釈の誤りが発見でき、エラーの早期解決が期待できます。

(静的チェックは通るが動的チェックでエラーになる例)

# typed: true
require 'sorbet-runtime'

class Example
  extend T::Sig

  def self.some_untyped_method
    nil
  end

  # NOTE: メソッドのシグネチャの型注釈は正しい
  sig {params(x: Integer).returns(Integer)}
  def self.add_one(x)
    x + 1
  end
end

# NOTE: 実行時には Integerの期待値に対してnilが渡されているためエラーになる
Example.add_one(Example.some_untyped_method)

このように想定されていない値がメソッドに渡された場合に、後続処理に渡す手前で早急なエラー検知できることが動的チェックの利点です。

実際の運用では、rspecなどでテストツール上でRutimeチェックを行うことで、コード信頼性の向上が期待できます。

Runtimeチェックの詳細については、以下の公式ページを参照ください。

https://sorbet.org/docs/runtime

tapiocaの概要

RBIを生成するためのCLIツールであるtapiocaについて、基本的な機能を記載します。

Sorbetを用いたRBIの型チェックを行う際、アプリケーション固有のRBIを定義する必要があります。しかし、アプリケーションが依存するライブラリの型情報を含め、全てを手作業で作成するのは困難です。そのため、基本的にはtapiocaを活用してこれらのRBIファイルを生成します。

tapiocaの型生成の主要コマンド

tapiocaはCLIツールであり、「tapioca <コマンド名>」という形式で使用します。tapiocaにおける型生成のための主要なコマンドは以下の3つです。

コマンド 説明
tapioca dsl Railsやその他のメタプログラミングに依存するDSLを解析して型を生成する
tapioca gem アプリケーションのGemfileに記載されているgemからRBIファイルを生成する
tapioca annotations コミュニティが提供する型注釈付きのRBIをダウンロードする

tapioca dsl

Rubyは動的にクラスやメソッドを生成することが可能であり、RailsのActiveRecordやActiveSupportではメタプログラミングを用いたメソッド定義が頻繁に行われます。

しかし、Sorbetはこのようなメタプログラミングを含むDSLを直接解析することができません。そこで、「tapioca dsl」というコマンドが、DSLによって生成された型を探索し、それらをRBIファイルとして出力する役割を果たします。

このコマンドを使用することで、例えばActiveRecordを継承したモデルの型を自動的に生成することができます。

「tapioca dsl」では、アプリケーションのファイルを直接ロードして解析を行うため、コマンドの実行時にはアプリケーションが正常に動作している状態である必要があります。

tapioca gem

tapioca dsl コマンドは、アプリケーションで使用されているgemの情報を基に、gem内部で定義されている依存関係の型定義をRBIファイルとして生成します。

RBIファイルはバージョン管理されており、gemのバージョンアップデート時にはRBIファイルの更新が必要になります。この更新作業を行う際には、tapioca gems --all コマンドを使用することで、全てのgem依存の型定義を一括で最新状態に更新することが可能です

bin/tapioca gems --all

tapioca annotations

tapioca gem コマンドで生成されるRBIファイルには、クラスやメソッドの定義が含まれていますが、sigを用いた型注釈は含まれていません。この型注釈を補うためのコマンドが tapioca annotations です。

tapioca annotations コマンドは、Shopifyが運営する rbi-central というリポジトリから、型注釈を含んだRBIファイルを取得してくれます。

(tapioca gem で出力したRBI)

...

# = Active Record \Core
#
# source://activerecord//lib/active_record/core.rb#10
module ActiveRecord::Core
  ...

  # @return [Boolean]
  #
  # source://activerecord//lib/active_record/core.rb#616
  def blank?; end
  
  ...
 ...
  • 型注釈がついていない

(tapioca annotations で出力したRBI 抜粋)

class ActiveRecord::Base
  sig { returns(FalseClass) }
  def blank?; end

  ...
  • 型注釈が付いている

終わりに

以上、Sorbetとtapiocaの基本知識について紹介しました。

PharmaX では、様々なバックグラウンドを持つエンジニアの採用をお待ちしております。
もし、興味をお持ちの場合は、私の X アカウント(@hakoten)や記事のコメントにお気軽にメッセージいただけますと幸いです。まずはカジュアルにお話できれば嬉しいです!

参考にした記事・ページ

https://techlife.cookpad.com/entry/2020/12/09/120454
https://qiita.com/getty104/items/9a2a20a6e170ab53191c

PharmaXテックブログ

Discussion