Rubyの型チェック入門——NoMethodErrorを防ぐSorbet、Steep、Type Profiler徹底解説
この記事は、ちょっとしたクイズから始めましょう。AppSignalがRubyアプリケーションで追跡している最も発生頻度の高いエラーは何だと思いますか?
多くの方がNoMethodErrorと答えたのではないでしょうか。これは、オブジェクトに存在しないメソッドを呼び出した際に発生する例外です。メソッド名のタイポが原因であることもありますが、より多いのは「誤った型のオブジェクト」に対してメソッドを呼び出したケースで、その多くは予期しないnilです。Ruby開発者として、こうしたエラーの発生頻度を減らすためにできることはあるのでしょうか?
型システムは救いの手になるのか?
テキストエディタやプログラミング言語の選択以外で、型システムの話題ほど瞬く間に白熱した議論になりやすいものはありません。ここで詳細に立ち入る余裕はありませんが、Chris Smithの記事「What To Know Before Debating Type Systems」がこの分野の優れた解説となっています。
大まかに分類すると、型システムには静的と動的という2つの大きなカテゴリがあります。前者は事前(コンパイラや専用ツールによって)に型を検査するのに対し、後者は実行時に検査が行われ、実際の型が開発者の期待と一致しない場合に例外が発生する可能性があります。
両陣営には強い主張がありますが、世の中には多くの誤解も流布しています。まず、静的型付けは大量の型アノテーションを必要としません。多くの最新のコンパイラは型を自動的に推論でき、これは「型推論」と呼ばれます。逆に、動的型付け言語が静的型付け言語と比べて著しく高い欠陥率を示すという証拠もありません。
ダックタイピング
Ruby自体は動的に型検査を行う言語であり、「ダックタイピング」というアプローチを採用しています。
アヒルのように歩き、アヒルのように鳴くなら、それはアヒルに違いない。
つまりRuby開発者は、オブジェクトの「型」そのものではなく、そのオブジェクトが特定の「メッセージ」(メソッド)に応答するかどうかを重視します。
では、なぜRubyでわざわざ静的型付けを考えるのでしょうか? コードが魔法のようにバグゼロになる万能薬ではありませんが、確かなメリットはいくつかあります。
- 正当性: 静的型付けは、前述の
NoMethodErrorのような特定の種類のバグを防ぐのに有効です。 - ツール: 開発時に静的な型情報があると、IDEのリファクタリング支援など、より充実した開発ツールを利用できます。
- ドキュメント: 静的型付け言語の多くは優れたドキュメント機構を備えています。HaskellのHoogleは型シグネチャから関数を検索できるエンジンを提供し、これを巧みに活かしています。
- パフォーマンス: コンパイラに渡る情報が多くなるほど、適用できる最適化の余地が広がります。
このリストは網羅的ではなく、各項目には反例も見つかりますが、一定の核心的真実を含んでいることは間違いありません。
段階的型チェック(Gradual Typing)
近年、「段階的型チェック(gradual type checking)」と呼ばれるアプローチが、JavaScript向けのTypeScript、PHP向けのHack、Python向けのmypyなど、さまざまな動的型付け言語に浸透しています。これらに共通するのは、オール・オア・ナッシングを要求せず、開発者の判断で変数や式に少しずつ型情報を追加していける点です。これは既存の大規模コードベースにとって特に有用で、システムの重要な部分だけを静的に検査し、残りは型なしのまま実行時検査に任せる、という運用が可能です。この記事で紹介するRubyの型チェックソリューションも、すべて同じアプローチに基づいています。
Rubyで使える主な選択肢
Ruby開発者がワークフローに静的型チェックを組み込みたい理由がわかったところで、現在人気のある選択肢を見ていきましょう。なお、Rubyへの静的型チェック導入は新しい発想ではありません。メリーランド大学の研究者たちは2009年にはすでにDiamondback Ruby(Druby)という拡張に取り組んでいましたし、2013年にはタフツ大学のプログラミング言語グループが「The Ruby Type Checker」という論文を発表し、それが型チェックと契約による設計(DbC)機能をライブラリとして提供するRDLプロジェクトへと発展しました。
Sorbet
Stripeが開発したSorbetは、現在最も注目を集めているRubyの型チェックツールです。クローズドベータの段階でShopify、GitLab、Kickstarter、Coinbaseといった大手企業が早期採用していたことも大きいでしょう。昨年のRubyKaigiで発表され、同年6月20日に初の公開リリースを迎えました。SorbetはモダンなC++で実装されており、Matzの意向(「私は型アノテーションが嫌いだ」という発言でも知られています)とは裏腹に、型アノテーションベースのアプローチを採用しました。特筆すべきは、Rubyの極めて動的な性質やメタプログラミング能力が静的解析の障壁になるため、静的検査と動的検査を組み合わせたハイブリッド方式を採っている点です。
# typed: true
class Test
extend T::Sig
sig {params(x: Integer).returns(String)}
def to_s(x)
x.to_s
end
end
型チェックを有効にするには、まず# typed: trueというマジックコメントを追加し、クラスにT::Sigモジュールをextendします。実際の型アノテーションはsigメソッドで指定します。
sig {params(x: Integer).returns(String)}
これは「このメソッドはInteger型の引数xを1つ受け取り、Stringを返す」という意味です。誤った型の引数で呼び出すとエラーになります。
Test.new.to_s("42")
# Expected Integer but found String("42") for argument x
Sorbetには、この基本チェック以外にも多彩な機能があります。例えば、恐怖のnilに対するNoMethodErrorから私たちを守ってくれます。
users = T::Array[User].new
user = users.first
user.username
# Method username does not exist on NilClass component of T.nilable(User)
上記のスニペットではUserオブジェクトの空配列を定義し、先頭要素(nilが返る)にアクセスしようとしています。Sorbetは「NilClassにはusernameというメソッドは存在しない」と正しく警告してくれるのです。一方、その値が決してnilにならないと確信できる場合は、T.mustでその旨をSorbetに伝えられます。
users = T::Array[User].new
user = T.must(users.first)
user.username
このコードは型検査を通過しますが、実行時例外を引き起こす可能性があるため、使用には注意が必要です。
そのほか、Sorbetにはデッドコード検出、型固定(一度文字列が代入された変数に整数を再代入できないようにするなど)、インターフェース定義などの機能もあります。
さらに、Sorbetは「Ruby Interface」ファイル(.rbi)とも連携でき、これらはカレントディレクトリのsorbet/フォルダに保持されます。これにより、プロジェクトが依存する全gemのインターフェース定義を生成でき、より多くの型エラーを発見する助けになります。
Sorbetには1記事では収まりきらないほど多くの機能(厳格さレベルの切り替えやメタプログラミング向けプラグインなど)がありますが、ドキュメントもすでに充実しており、改善のためのPRも歓迎されています。
Steep
Sorbetの代替として最も知られているのが、松本宗太郎(Soutaro Matsumoto)氏によるSteepです。アノテーションを使わず、独自の型推論も行いません。代わりに、sigディレクトリ内の.rbiファイルに完全に依存します。
次のシンプルなRubyクラスを例にしましょう。
class User
attr_reader :first_name, :last_name, :address
def initialize(first_name, last_name, address)
@first_name = first_name
@last_name = last_name
@address = address
end
def full_name
"#{first_name} #{last_name}"
end
end
次のコマンドで、初期状態のuser.rbiファイルの雛形を生成できます。
$ steep scaffold user.rb > sig/user.rbi
生成されるのはあくまで「出発点」となるファイルです。すべての型がany(安全性を一切保証しない型)として指定されていることからもそれがわかります。
class User
@first_name: any
@last_name: any
@address: any
def initialize: (any, any, any) -> any
def full_name: () -> String
end
しかし、このまま型検査を実行すると、エラーが出ます。
$ steep check
user.rb:11:7: NoMethodError: type=::User, method=first_name (first_name)
user.rb:11:21: NoMethodError: type=::User, method=last_name (last_name)
これは、Steepがattr_readerで定義されたメソッドを認識するために特別なコメントを必要とするためです。そこでコメントを追加します。
# @dynamic first_name, last_name, address
attr_reader :first_name, :last_name, :address
さらに、生成済みの.rbiファイルに各メソッドの定義を追記します。ついでに、シグネチャもanyから実際の型へ変更しておきましょう。
class User
@first_name: String
@last_name: String
@address: Address
def initialize: (String, String, Address) -> any
def first_name: () -> String
def last_name: () -> String
def address: () -> Address
def full_name: () -> String
end
これですべて期待どおりに動作し、steep checkはエラーを返さなくなります。
加えて、Steepはジェネリクス(例:Hash<Symbol, String>)やユニオン型(複数の型のいずれかを表す型)もサポートしています。例えば、ユーザーのtop_postメソッドは、そのユーザーが書いた最高ランクの投稿を返す一方、未投稿の場合はnilを返すかもしれません。これはユニオン型(Post | nil)で表現され、対応するシグネチャは次のようになります。
def top_post: () -> (Post | nil)
SteepはSorbetほど機能豊富ではありませんが、十分に有用なツールであり、MatzがRuby 3の型チェック構想として描いていた姿に近い存在と言われています。
Ruby Type Profiler
Cookpadの遠藤侑介(Yusuke Endoh、Rubyコミュニティでは「mame」として有名)氏は、「レベル1型チェッカー」と位置づけられるRuby Type Profilerを開発しています。ここで紹介した他のツールと違い、シグネチャファイルも型アノテーションも不要で、プログラムを解析しながら可能な限り型を推論します。SteepやSorbetに比べて検出できる問題は少ないものの、開発者側に追加のコストが一切かからないのが魅力です。
まとめ
将来を正確に予測することは誰にもできませんが、Rubyにおける型チェックは今後も定着していくものと見られます。現在、.rbiファイル(Ruby Type Profilerで雛形生成も可能)で使うための共通記法「Ruby Signature Language」の標準化が進められており、後にRBSとして結実しました。これにより、開発者は好みのツールを自由に選べるようになっています。Steepはライブラリ作者がgemに型情報を同梱できる仕組みをすでに備えており、Sorbetにも、TypeScriptのDefinitelyTypedリポジトリに着想を得たsorbet-typedという類似の仕組みがあります。Rubyの型チェックの未来を形作ることに興味があるなら、今こそ参画する絶好のタイミングです!
-
PCの動作が遅い?ハードウェア交換の前に確認したい10の対処法
最近パソコンの動きが妙に遅くなったと感じたら、買い替えや廃棄を考える前に、まず確認しておきたい項目がいくつかあります。動作が遅くなる原因はソフトウェア側の場合も、ハードウェア側の場合もありますが、問題発生前に物理的な損傷がなければ、原因はソフト側である可能性が高いです。この記事では、PCの低速化をハードウェアのせいにする前に確認すべき事柄を順番に解説します。多くの場合、ソフトウェア面での対策だけで問題は解決します。それでも改善しなければ、初めてハードウェアの交換やアップグレードを検討すればよいのです。 1. まずは再起動 「明らかに普段より遅い」と感じているなら、おそらく再起動はすでに試してい
-
Windows 10でRAMの種類を確認する4つの方法【DDR・DDR2・DDR3・DDR4の見分け方】
RAM(Random Access Memory:ランダムアクセスメモリ)は、現在のパソコンやスマートフォンに搭載されている最も重要なパーツの一つです。デバイスの動作速度やパフォーマンスの良し悪しを左右する要素であり、ユーザー自身で増設・交換できる数少ないパーツでもあります。用途に応じてRAM容量を自由に増やせる点が大きな魅力です。一般的に、ライトユーザーから中程度のユーザーには4GB〜8GB程度のRAM容量が適しており、それ以上の大容量は動画編集やゲームなど高負荷な用途で活躍します。コンピューターの進化とともにRAMも大きく変化してきました。特に注目すべきは「RAMの種類(世代)」です。「自