Sorbetを使ってRubyでインタフェース機能を活用する
この記事の概要
こんにちは。PharmaX でエンジニアをしている諸岡(@hakoten)です。
この記事では、Rubyの型チェックツール「Sorbet」のインタフェース機能に焦点を当てて解説しています。Sorbetの型チェックに興味がある方や、導入を検討している方の参考になれば幸いです。
Sorbetとは
Sorbet とはStripe社が開発を主導するRuby言語に後から導入するための型チェックツールです。
Sorbetでは、主に「静的チェック」「動的(Runtime)チェック」の2つの型チェック機能を提供していて、動的言語であるRubyの構文に対して、TypeScriptのtsc
コマンドのように「静的な型チェック」をかけることができるのが特徴です。
この記事では、Sorbetの全体的な詳細には触れません。Sorbetの基本について知りたい方は、ぜひ以下の記事もご覧ください。
Sorbetを使うメリット
前述の通り、Sorbetには「ランタイム上で行われる動的チェック」と「コマンドから実行する静的チェック」という2つの主要な機能があります。特に、従来のRubyにはない「静的チェック」は、Sorbetの使用における大きなメリットの一つです。ここで静的チェックについて簡単に説明します。
例えば、以下のようなRubyコードがあるとします。
class Test
def sum(a, b)
a + b
end
end
通常のRubyでは、もしa
やb
が+
メソッドを持たないインスタンスであれば、例えばnil
が渡された場合、実行時にNoMethodError
が発生します。つまり、エラーは「コードを実行するまで」検知されません。
しかし、Sorbetを使用すると、「コード実行前(例えばgitのコミット時やCI時)」にエラーを検知できるようになります。以下はSorbetを使用して型を明示した例です。
# typed: true
class Test
extend T::Sig
sig { params(a: Integer, b: Integer).returns(Integer) }
def sum(a, b)
a + b
end
end
sorbet tc
静的チェックは、コマンドで実行できる
Sorbetでは、メソッドにデータ型のシグネチャを宣言することができます。このメソッドシグネチャを使用することで、TypeScriptや他の型付き言語のように、実行前に引数や戻り値の型が正しいかをチェックできます。これにより、実行前にバグを早期に発見し、より堅牢なコードを作成することが可能です。
Sorbetのインタフェース
Sorbetのインタフェース機能は、他言語のインタフェースと同様、クラスが提供すべきメソッドのセットを定義するために用います。この機能により、異なるクラスのインスタンスを同一のインタフェース型として扱うことが可能になり、多様性(ポリモーフィズム)を実現できます。
例として、Javaにおけるインタフェースの使用例を以下に示します。
public interface Animal {
public void bark();
}
public class Dog implements Animal {
public void bark() {
System.out.println("Bowwow!");
}
}
public class Cat implements Animal {
public void bark() {
System.out.println("Meow!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.bark();
cat.bark();
}
}
Rubyでもポリモーフィズムを実現できますが、Rubyにはインタフェースという言語仕様はありません。しかし、Sorbetを使用すると、他の静的型付け言語に似た形でインタフェースを実装できるようになります。
Sorbetを使うことのもう一つの利点は、静的チェックを通じてより安全なコードを保証できる点です。次に、Sorbetのインタフェース機能の具体的な挙動と使い方を詳しく見ていきましょう。
Sorbetを使わずRubyだけで書いた例
まず、Sorbetを使わないRubyだけで書いた例を示します。
class Cat
def bark
puts 'Meow!'
end
end
class Dog
def bark
puts 'Bowwow!'
end
end
class Test
def call_bark(animal)
animal.bark
end
end
Test.new.call_bark(Dog.new)
Test.new.call_bark(Cat.new)
Bowwow!
Meow!
この例では、CatとDogクラスが共通のメソッドbark
を実装しています。Rubyのような動的型付け言語では、ダックタイピングという手法が一般的です。ダックタイピングは、「同じ振る舞い(同じメソッドを持つ)をするオブジェクトなら、その型に関係なく扱うことができる」という考え方に基づいています。
この例で、CatとDogは異なるクラスですが、どちらもbark
メソッドを持っているため、Testクラスのcall_bark
メソッド内で問題なく呼び出すことが可能です。Sorbetを使わない標準的なRubyのアプローチでは、このようなダックタイピングを利用してポリモーフィズムを実現します。
Sorbetでインタフェースを使用せずに書いた場合
次にSorbetのインタフェース機能は使わず、メソッドシグネチャのみ使った場合の例を示します。
# typed: true
class Animal
extend T::Sig
sig { void }
def bark
raise NotImplementedError, 'You must implement the bark method'
end
end
class Cat < Animal
extend T::Sig
sig { void }
def bark
puts 'Meow!'
end
end
class Dog < Animal
extend T::Sig
sig { void }
def bark
puts 'Bowwow!'
end
end
class Test
extend T::Sig
# 関数シグネチャで型を付けることで、引数の型を明示的に宣言できる
sig { params(animal: Animal).void }
def call_bark(animal)
animal.bark
end
end
Test.new.call_bark(Dog.new)
Test.new.call_bark(Cat.new)
Testクラスのcall_barkメソッドにシグネチャをつけるため、基底クラスとしてAnimalクラスをDog、Catで継承しています。
Sorbetのメソッドシグネチャを定義することで「想定していないデータ型の引数が渡されてしまう」ことを静的にチェックが可能です。
例として、次のようなコードを試してみます。
# 本来はAnimal型のインスタンスが必要ですが、整数を渡す
Test.new.call_bark(1)
本来、call_barkにはAnimal型が渡されることを期待していますが、Integerを引数に渡しています。
この場合、Sorbetのsrb tc
コマンドを使用すると、引数の型が不正であるとしてエラーが検出されます。
% bundle exec srb tc .
./app/test_interface/example.rb:7: Expected Animal but found Integer(1) for argument animal https://srb.help/7002
7 | Test.new.call_bark(1)
^
Expected Animal for argument animal of method Test#call_bark:
./app/test_interface/test.rb:6:
6 | sig { params(animal: Animal).void }
^^^^^^
Got Integer(1) originating from:
./app/test_interface/example.rb:7:
7 | Test.new.call_bark(1)
^
Errors: 1
引数の事前チェックができるのであれば、このままでも問題ないのでは?と思うかもしれませんが、これだけでは不十分な場合があります。例えば、以下のようにDogクラスにbarkメソッドが意図的に実装されていない場合、現在のSorbetの設定では静的チェックでエラーを検出できません。
class Dog < Animal
extend T::Sig
# あえてbarkを実装しない
end
% bundle exec srb tc .
No errors! Great job.
静的チェックでエラーが発生しない
[1] pry(main)> Test.new.call_bark(Dog.new)
NotImplementedError: You must implement the bark method
from /myapp/app/test_interface/animal.rb:8:in `bark'
実行時には、メソッドが定義されていないため、エラーになる
この問題は、メソッドシグネチャを宣言しただけでは実装の存在が保証されないため発生します。
このような挙動を解決するために、次に説明する抽象メソッド(abstract method)やインタフェース(interface)を使います。
Sorbetでインタフェースを使う場合
ここからは、Sorbetの抽象メソッド(abstract method)とインタフェース(interface)機能を使った方法について記載します。
インタフェース・抽象メソッドの書き方
Sorbetでは、実装を伴わない抽象メソッドを宣言するために、メソッドシグネチャに abstract
キーワードを使用します。また、モジュールをインタフェースとして扱うには、interface!
キーワードを使用して宣言します。
※ Sorbetでは、別に抽象クラスとして定義も可能ですがここではinterface!のみ扱います。
構文としては次のようになります。
# typed: true
module Animal
extend T::Sig
# ① T::Helpersをextends
extend T::Helpers
# ② interface!を宣言する
interface!
# ③ abstractを付けたシグネチャを宣言
sig { abstract.void }
def bark
# ④ 空実装にする
end
end
まず、モジュールの定義としては、次の2点が必要です。
- ①
T::Helpers
をextendsする - ②
interface!
を宣言する
抽象メソッドとしては「③ abstract
を付けた関数のシグネチャを宣言する」必要があります。
abstractメソッドは、基本的に実装を許さないため、④ ※ メソッド中身は空
である必要があります。
※ メソッドの中身を書くと静的チェックでエラーになります
抽象メソッドを実装する
# typed: true
class Dog
extend T::Sig
# ① インタフェースとして作成したモジュールをincludeする
include Animal
# ② overrideを付けたメソッドシグネチャを宣言
sig { override.void }
def bark
puts 'Bowwow!'
end
end
抽象メソッドを実装する側は、次のようにします。
- ① インタフェースとして作成したモジュールをincludeする
- ②
abstract
でメソッドシグネチャを宣言したメソッドに対して、override
を付けてメソッドシグネチャを宣言する
インタフェースとして宣言したモジュールをincludeしたクラスは、モジュールの持つ抽象メソッドに対してoverrideを付けた実装がないとエラーになります。(動的チェック及び静的チェック両方でエラーになります)
この制約のおかげで、「対象のインスタンスにメソッドが存在しているか?」 という実装保証を事前にチェックすることが可能になります。
公式ページ
その他使い方の詳細は次のSorbet公式ページを参照ください。
インタフェースを使った完成例
最後にインタフェースを使った完成例は次のとおりです。
# typed: true
module Animal
extend T::Sig
extend T::Helpers
interface!
sig { abstract.void }
def bark
end
end
class Cat
extend T::Sig
include Animal
sig { override.void }
def bark
puts 'Meow!'
end
end
class Dog
extend T::Sig
include Animal
sig { override.void }
def bark
puts 'Bowwow!'
end
end
class Test
extend T::Sig
sig { params(animal: Animal).void }
def call_bark(animal)
animal.bark
end
end
Test.new.call_bark(Dog.new)
Test.new.call_bark(Cat.new)
インタフェースを使わない場合にエラーを検知できなかった、Dogクラスにbarkメソッドを実装しないケースを試してみましょう。
class Dog
extend T::Sig
include Animal
# あえてbarkを実装しない
end
bundle exec srb tc .
./app/test_interface/dog.rb:3: Missing definition for abstract method Animal#bark https://srb.help/5023
3 |class Dog
^^^^^^^^^
./app/test_interface/animal.rb:9: bark defined here
9 | def bark
^^^^^^^^
Autocorrect: Use -a to autocorrect
./app/test_interface/dog.rb:8: Insert sig { override.void }
def bark; end
8 |end
^
Errors: 1
このようにインタフェースを使うことで、srb tc
コマンドにより、実行前に実装の不備を発見することができるようになります。
終わりに
以上、Sorbetのインタフェース機能について紹介でした。
PharmaXでは、様々なバックグラウンドを持つエンジニアの採用をお待ちしております。
Sorbetを使ったRubyのアプリケーション開発に興味がある方はぜひお声がけください。
弊社はAI活用にも力を入れていますので、LLM関連の開発に興味がある方もお待ちしております。
もし興味をお持ちの場合は、私のXアカウント(@hakoten)や記事のコメントにお気軽にメッセージいただければと思います。まずはカジュアルにお話できれば嬉しいです!
PharmaXエンジニアチームのテックブログです。エンジニアメンバーが、PharmaXの事業を通じて得た技術的な知見や、チームマネジメントについての知見を共有します。 PharmaXエンジニアチームやメンバーの雰囲気が分かるような記事は、note(note.com/pharmax)もご覧ください。
Discussion