SorbetとMiniTestでRubyに入門する
GitHub
コードはこちらに載せています.docker compose up -d
で立ち上がるので煮るなり焼くなりどうぞ.スター⭐️もらえるとモチベーションに繋がります!
TL;DR
知見だけまとめます.
- Sorbetが良い.他の言語と同じ感覚で書ける
- Sorbet Runtimeで実行時解析が効いて嬉しい
- MiniTestでシンプルにテストを書ける
注意
Rubyガチ初心者です.普段はPython, Rust, PHPをメインに書いています.
使用したツール
パッケージマネージャー: Bundler
静的解析ツール: Sorbet CLI, RoboCop
テストツール: MiniTest
実行時解析: Sorbet Runtime
※ Bundlerのリンク先がgemになっているのはBundlerのレポジトリがPublic Archiveになっていてgemのレポジトリに管理先が移行しているからです
Bundler is now maintained in the rubygems/rubygems repository.
ざっくりとした書き方
main.rbファイルはこのような感じになっています.
# typed: strict
require 'sorbet-runtime'
require_relative 'service/fizz_buzz_runner'
class Program
extend T::Sig
sig { void }
def self.main
loop = Loop.new(1, 30)
fizz_buzz_runner = FizzBuzzRunner.new(loop: loop)
fizz_buzz_runner.run
end
end
Program.main
上から順番に説明すると
-
#typed: strict
: 静的解析用のコメントアウトです.-
typed: ignore
,typed: false
,typed: true
,typed: strict
,typed: strong
の5段階あります. - 私も書いている時にstrongがあることを思い出しました.PHPだと
declare(type=strict)
と書くので,癖かもしれないです. - https://sorbet.org/docs/static
-
-
require 'sorbet-runtime'
: runtimeでの型チェックを有効化します. -
extend T::Sig
,sig { void }
- Sorbetの機能でコードにsignatureをつけることができます
- ここでは特に制御するものがなかったので
void
ですが,parameters
やreturns
といった入出力を制御することができます
-
fizz_buzz_runner.run
- Rubyでは引数のない関数には括弧をつけないです.
- 地味に他では経験したことのない文法でした.
Static Analysis
Sorbetを使っています.Sorbetを使うとRubyのコード上に型情報を付与できます.
SorbetはRBSとは異なり,サードパーティ製ツールです.しかし,開発元がStripeで,Shopify社やGitHub社といった「Rubyを使っている大企業」で想起されるような会社が採用をしています.そして,Repositoryを確認したところ,直近の更新(2024.07.11時点)もlast weekと開発も進んでいるようです.以上の理由から突然なくなることもないかなと判断し採用しました.
# frozen_string_literal: true
# typed: strict
require 'sorbet-runtime'
require_relative '../../app/domain/loop'
require_relative '../../app/domain/printer/fizz'
require_relative '../../app/domain/printer/buzz'
require_relative '../../app/domain/printer/fizz_buzz'
require_relative '../../app/infrastructure/message_provider'
class FizzBuzzRunner
extend T::Sig
sig { returns(Loop) }
attr_reader :loop
sig { returns(MessageProvider) }
attr_reader :message_provider
sig { params(loop: Loop, message_provider: MessageProvider).void }
def initialize(
loop: Loop.new(1, 100),
message_provider: MessageProvider.new
)
@loop = T.let(loop, Loop)
@message_provider = T.let(message_provider, MessageProvider)
end
sig { void }
def run
(@loop.start_with..@loop.end_with).each do |i|
puts @message_provider.provide(i)
end
end
end
Test
MiniTestを採用しています.以下のように書くことでtest
dir以下の_test
postfixがついたファイルを実行できます.
bundle exec ruby -Itest -e 'Dir.glob("./test/**/*_test.rb") { |file| require file }'
MiniTestはRSpecと比較していますが,個人的な好みで採用したので,特別強い信念があるというわけではないです.
実装例
ここではFizzBuzzのiter変数ごとに出力を切り替えるプログラムを実装しています.入力はInteger型で出力はString型です.
# frozen_string_literal: true
# typed: strict
require 'sorbet-runtime'
require_relative '../domain/printer/fizz'
require_relative '../domain/printer/buzz'
require_relative '../domain/printer/fizz_buzz'
class MessageProvider
extend T::Sig
sig { void }
def initialize
@fizz = T.let(Fizz.new, Fizz)
@buzz = T.let(Buzz.new, Buzz)
@fizz_buzz = T.let(FizzBuzz.new, FizzBuzz)
end
sig { params(counter: Integer).returns(String) }
def provide(counter)
if (counter % 3).zero? && (counter % 5).zero?
@fizz_buzz.provide_message
elsif (counter % 3).zero?
@fizz.provide_message
elsif (counter % 5).zero?
@buzz.provide_message
else
counter.to_s
end
end
end
テスト例
実装と同じように Sorbet
を使ってSignatureを記載します.テストファイルにはminitest/autorun
を追加します.assertなどは他の言語と同様に行います.
# frozen_string_literal: true
# typed: strict
require 'minitest/autorun'
require 'sorbet-runtime'
require_relative '../../app/infrastructure/message_provider'
class MessageProviderTest < Minitest::Test
extend T::Sig
sig { void }
def setup
@message_provider = T.let(MessageProvider.new, T.nilable(MessageProvider))
end
sig { void }
def test_show_fizz_when_counter_is_three
test_case = 3
assert_equal('Fizz', T.must(@message_provider).provide(test_case))
end
sig { void }
def test_show_buzz_when_counter_is_five
test_case = 5
assert_equal('Buzz', T.must(@message_provider).provide(test_case))
end
sig { void }
def test_show_fizz_buzz_when_counter_is_fifteen
test_case = 15
assert_equal('FizzBuzz', T.must(@message_provider).provide(test_case))
end
sig { void }
def test_show_fizz_buzz_when_counter_is_multiple_of_three_and_five
test_case = 30
assert_equal('FizzBuzz', T.must(@message_provider).provide(test_case))
end
sig { void }
def test_show_counter_when_counter_is_not_divisible_by_three_or_five
test_case = 1
assert_equal('1', T.must(@message_provider).provide(test_case))
end
end
Rubyのシンプルな文法のおかげでコードが読みやすいです.
書いてみた感想
初めてRubyを書いてみて,静的解析に従っていけばコードの一貫性を保ちながら高速にプログラムを作成できて楽しかったです.2スペースインデントも最初は慣れなかったですが,ネストが深くなるところでtabより読みやすいなあと感じました.以下,各ライブラリの感想です.
- Sorbetの静的解析を通ったコードは一貫性を持っていて非常に読みやすい.Sorbet特有の書き方を理解するのが大変だが,頑張る価値はあると思う.
- Sorbet Runtimeで実行時型解析が効いて嬉しい.PythonだとClassと関数で別のライブラリを使うので結構大変だったりする.
- MiniTestでシンプルにテストを書ける.テストコードの複雑度が上がらないようになっていて非常に嬉しい.
めっちゃどうでもいいですが,ZennのアイキャッチでRubyと入れるとダイヤが出てくる仕様が地味にツボでしたw
Discussion