ISUCON で型がパチパチっとハマった開発ができるとかなり開発体験変わってくるのでは?と思い、 ISUCON の過去問に型をつけていくのをやってみています。
モチベーションに対してもう少し詳しい記事はこちら
ISUCON13 で rbs-inline 使ってみた - カレーの恩返し に引き続き ISUCON12予選 の Ruby 実装に対して rbs-inline を使ってみました。
やっぱりライブラリの型が足りない
ライブラリの型はいくつか足りないものがあったので追加しました。しかし、sinatra gem や mysql2 gem はちょっとした変更だけで済んだので ISUCON13 での型付けの恩恵が受けられました。
また、前回は stdlib への型定義は行っていなかったのですが、今回はチャレンジしてみました。gem_rbs_collection よりも周辺環境が手厚い印象を受けました。
ref.
- Add Sinatra::Cookies signature by euglena1215 · Pull Request #586 · ruby/gem_rbs_collection · GitHub
- Add empty Sinatra::Reloader signature by euglena1215 · Pull Request #587 · ruby/gem_rbs_collection · GitHub
- mysql2: Add Mysql2::Error by euglena1215 · Pull Request #666 · ruby/gem_rbs_collection · GitHub
- sqlite3: Add database and pragmas by euglena1215 · Pull Request #667 · ruby/gem_rbs_collection · GitHub
- stdlib: Add types for CSV#headers by euglena1215 · Pull Request #2012 · ruby/rbs · GitHub
- stdlib: Add types for Open3 by euglena1215 · Pull Request #2014 · ruby/rbs · GitHub
rbs-inline の Data, Struct サポートで足りない表現がある
ISUCON13 の型付けでは色々頑張って Data.define
によるクラス定義の型を書いていましたが、soutaro さんに熱意を伝えて rbs-inline で Data, Struct をサポートしてもらいました 🎉*1
ですが、ISUCON12 予選の Data, Struct の使い方は rbs-inline のカバー範囲を超えていたため、いくつか patch を投げました。これらは取り込まれて rbs-inline 0.8.0 で使えるようになっています。
ref.
- Include `self.members` and `members` method definitions in RBS generated by `Data.define` by euglena1215 · Pull Request #96 · soutaro/rbs-inline · GitHub
- Add overload for `self.new` method when using `keyword_init: true` with `Struct.new` by euglena1215 · Pull Request #99 · soutaro/rbs-inline · GitHub
上記の Struct の使い方に関連して、ISUCON12 予選の型検査を通すためにいくつか steep:ignore
をつけて型検査を無効化しました。
# 大会を取得する #: (SQLite3::Database[SQLite3::result_as_hash], String) -> CompetitionRow? def retrieve_competition(tenant_db, id) # row は Hash[String, Integer | String | Float | nil] row = tenant_db.get_first_row('SELECT * FROM competition WHERE id = ?', [id]) if row CompetitionRow.new(row) # steep:ignore else nil end end
CompetitionRow
は Struct.new
によって定義されたクラスであり、row
は String
を key に持ち Integer | String | Float | nil
を value とする Hash です。
また、Struct は keyword_init: true
をつけたときのみ self.new
に渡した Hash を展開し、キーワード引数を受け取ったような挙動をします。
irb(main):004> Foo = Struct.new(:x, :y, keyword_init: true) => Foo(keyword_init: true) irb(main):005> Bar = Struct.new(:x, :y) => Bar irb(main):006> Foo.new({x: 1}) => #<struct Foo x=1, y=nil> irb(main):007> Bar.new({x: 1}) => #<struct Bar x={:x=>1}, y=nil> irb(main):008> Baz = Struct.new(:x, :y, keyword_init: false) => Baz irb(main):009> Baz.new({x: 1}) => #<struct Baz x={:x=>1}, y=nil>
CompetitionRow.new
は正確な Hash のリテラル({ ?tenant_id: String, ?id: String, ?title: String, ?finished_at: Integer, ?created_at: Integer, ?updated_at: Integer }
)を要求するのに対し、row
は Hash[String, Integer | String | Float | nil]
であり具体的な attribute の情報を持っていません。
その結果、以下の型エラーが起きていました。
Cannot pass a value of type `::Hash[::String, ::SQLite3::row_value_type]` as an argument of type `{ }` ::Hash[::String, ::SQLite3::row_value_type] <: { } (Ruby::ArgumentTypeMismatch)
それぞれの attribute に対して丁寧にチェックを行い、最終的にキャストするようにすれば ignore する必要はないのかもしれないですが ISUCON 本番でそこまでできるとは到底思えません。なので、 ignore を行う判断をしました。
総評して以前 ISUCON13 の型をつけたときよりも楽になっているなと感じました。これはライブラリの型が増えたことや rbs-inline に Data, Struct サポートが入ったことに起因していると思います。この調子で ISUCON 本番までにできる限りの過去問に型を付けられるようにしていきたいなと思います。
*1:自作 gem とかも作ってみてたんですがスッと入れてもらえたので出番はありませんでした https://github.com/euglena1215/rbs_inline_data