ArticlesRuby programming

Sorbet T.must_because

A small contribution to Sorbet I made

A couple of weeks ago I landed a small addition to Sorbet T.must_because. Out of context the method name sounds a bit odd but it's been super useful to have around.

The before times

Since I started using Sorbet, it has had a plain T.must method, which narrows a nil-able type:

int = T.must(int_or_nil)

But these were infuriating to me:

  • as a code reviewer
  • as someone who reads code
  • as someone who needs to fix bugs on these calls

The error message T.must raises when a nil is passed is simply:

TypeError: Passed `nil` into T.must

Adding context

What I have wanted for a long time is a place to provide context. With T.must_because we have nice friendly and concise way of doing just that:

sig {params(list: T::Array[String]).returns(String)}
def human_readable_second_item(list)
  if list.size < 2
    return "List not long enough"
  end

  item = T.must_because(list[1]) {'we check the size above'}
  "Second item: #{item}"
end

Despite not having worked with C++ or Sorbet's test suite before, the process was pretty straight forward to land the feature. Check out the pull request on Github!

The T.must_because method is gaining steam internally at Stripe and I recommend giving it a try in other codebases. I personally have no reason to use T.must ever again. Context, especially when you're escaping the type system, is always good.

Interesting things I learned in the process

  • Sorbet is all about those integration tests. Most of the tests for Sorbet are on the level of textual output and uses a pseudo language for making assertions about what errors are expected. The tests were really cool being so tangible, definitely a good way to avoid stressing on internal details.

  • All Ruby methods take an implicit block so a block doesn't add performance overhead in itself. In evaluating call signatures for this method I learned from the Sorbet Slack channel that the {"msg"} syntax was optimal. Coming from JavaScript where function allocations need to be considered in performance-sensitive code, this was surprising. This definitely seems like a place where the Sorbet compiler would be able to optimize to an extent such that it would become perf-sensitive.