カレーの恩返し

おいしいのでオススメ。

ISUCON12 予選で rbs-inline 使ってみた

ISUCON で型がパチパチっとハマった開発ができるとかなり開発体験変わってくるのでは?と思い、 ISUCON の過去問に型をつけていくのをやってみています。

モチベーションに対してもう少し詳しい記事はこちら

euglena1215.hatenablog.jp

ISUCON13 で rbs-inline 使ってみた - カレーの恩返し に引き続き ISUCON12予選 の Ruby 実装に対して rbs-inline を使ってみました。

やっぱりライブラリの型が足りない

ライブラリの型はいくつか足りないものがあったので追加しました。しかし、sinatra gem や mysql2 gem はちょっとした変更だけで済んだので ISUCON13 での型付けの恩恵が受けられました。
また、前回は stdlib への型定義は行っていなかったのですが、今回はチャレンジしてみました。gem_rbs_collection よりも周辺環境が手厚い印象を受けました。

ref.

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.

上記の 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

CompetitionRowStruct.new によって定義されたクラスであり、rowString を key に持ち Integer | String | Float | nilvalue とする 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 })を要求するのに対し、rowHash[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