If you’re interested in seeing the code for this post it’s all here.
The first thing I did was add
sorbet and the
This also installs the gem
sorbet-static, a dependency of
I then ran
srb init. This took around 30 seconds to generate a sorbet directory
containing 20 directories, 112 files. It also added
# typed: true to 125 files
# typed: false to 108 files - a 53.64% success rate.
The majority of the failures were config files and specs. But there were some
important files missed, too, including search_controller and finders_controller,
and a couple of models. These files likely were skipped as they’re a bit complex
(which is probably my bad). It also added
typed: strong and
to a few files (most of them within the newly created
init command completed, it printed a helpful suggestion on what to
srb tc which typechecks the project!
$ bundle exec srb tc
No errors! Great job.
Starting to check types
So what did
srb tc actually do? From what I can gather, sorbet looks for constant
resolution errors. I understand most of what is written about those errors
in sorbet’s docs.
Sorbet looks at every file except those with
typed: ignore at the top.
More in the docs.
It’ll catch things like NoMethodErrors before they turn into runtime errors, which is very cool.
For example, I added
call_non_existant_method to the end of a method in the
UrlBuilder class and the typechecker caught it. This would not have been
caught unless this method had been called in a test, or a user had hit this.
$ bundle exec srb tc
app/lib/url_builder.rb:15: Method call_non_existant_method does not exist on UrlBuilder https://srb.help/7003
15 | call_non_existant_method
I looked at the docs for adopting sorbet, and found this under running
the first time:
Step 4: Fix constant resolution errors At this point, it’s likely that there are lots of errors in our project, but Sorbet silences them by default. Our next job is to unsilence them and then fix the root causes.
Later it says
Step 4 was the biggest hurdle to adopting Sorbet
So it looks like adopting sorbet is easier than it looks for finder-frontend. A caveat is that this is a fairly simple rails app.
Importantly, Sorbet does not yet report type errors The final step is to start enabling more type checks in our code
OK. So I’ll start adding
typed: true and adding method signatures.
I’ll start with the
CacheableRegistry module, as I worked on it recently.
typed: false to
typed: true in this file yields the following error.
bundle exec srb tc
app/lib/registries/cacheable_registry.rb:25: Method raise does not exist on Registries::CacheableRegistry https://srb.help/7003
25 | raise NotImplementedError, "Please supply a cacheable_data method"
Interestingly, trying this out on sorbet.run does not yield the same thing.
To resolve this, I need to
include Kernel in the module. I suppose this
will be necessary on every class that calls
raise. This seems a shame since
the Kernel module is already included in every ruby
Object class (cite), though I understand one can override methods like
sleep if you really wanted to.
A cool thing happened when I changed the
CacheableRegistry to a class, when
messing around with the raise issue. I got this error when running
sorbet/rbi/hidden-definitions/hidden.rbi:24230: Registries::CacheableRegistry was previously defined as a class https://srb.help/4012
24230 |module Registries::CacheableRegistry
24231 | extend ::T::Sig
app/lib/registries/world_locations_registry.rb:3: Only modules can be included. This module or class includes Registries::CacheableRegistry https://srb.help/5032
3 | class WorldLocationsRegistry < Registry
This is great, since it tells me everywhere that I need to change the usage of this module if I do want to go ahead with changing it.
While I’m looking at this module, I wonder if I can get it to be
Making that change throws some expected errors. Strict mode requires that all methods have sigs, and all variables must have a type.
$ bundle exec srb tc
app/lib/registries/cacheable_registry.rb:6: This function does not have a `sig` https://srb.help/7017
6 | def can_refresh_cache?
Autocorrect: Use `-a` to autocorrect
But, that’s exciting, running it with the
-a flag fixes half of them.
can_refresh_cache? method has a signature (sig) above it!
This is great. What about the other methods? The
refresh_cache method should
return a boolean, if we’ve been able to cache our data. At the moment we’re
seeing an error in strict mode:
app/lib/registries/cacheable_registry.rb:10: This function does not have a `sig` https://srb.help/7017
10 | def refresh_cache
To fix this we can add
T::Boolean as the return type in the sig.
rescue GdsApi::HTTPServerError, GdsApi::HTTPBadGateway
What’s quite nice is how much sorbet reveals about it’s internals as you work with it. T::Boolean, when recommended, shows you that it’s an alias.
T::Boolean = T.type_alias(T.any(TrueClass, FalseClass))
With a couple of other easy fixes, CacheableRegistry is now strictly typed.
This has some benefits, but you can read about those in the sigs docs.
What I like about all of this is that it’s all valid ruby. It makes for a much simpler adoption.
With the route that the team behind sorbet have taken, it’s much easier to introduce type checks, without the pain of feeling like you’re performing a migration. It’s still a migration, but one that takes minutes, not months.
I like that the sorbet’d (?) code isn’t going to be transpiled into something
different to what you’ve written. It’s all still valid ruby, just with
some annotations. But they’re far superior to comments, as the static
sorbet tc ensures the annotations can’t become stale.
Sorbet is still a fairly young project, and I probably can’t introduce it into projects at work yet. That said, on GOV.UK we have our fair share of runtime errors that would be caught if we had a type checker. I’m quite excited about sorbet and the direction that it is headed, and will see if I can use sorbet in my own projects.
Good luck to the sorbet team!