Two insights from using Sorbet
I am working with Sorbet on a relatively big Ruby on Rails project, and here are two quick learnings:
It can help with simplifying code by removing unnecessary transformations.
The support that Ruby has for doing things in multiple ways is very helpful when hitting a case where Sorbet has a rough edge
Let's take them step by step:
How to simplify methods by removing unnecessary transformations
Here is a piece of code that is a public interface of an object (this is not the actual code but an approximation of the actual code)
# typed: strict
ACCEPTED_KEYS = T.let(%i[name username email].freeze, T::Array[Symbol])
sig { params(attributes: T::Hash[T.any(Symbol, String), Integer]).returns(T::Hash[Symbol, Integer]) }
def accepted_attributes(attributes)
attributes
.symbolize_keys
.slice(*ACCEPTED_KEYS)
end
Let's first focus on the .symbolize_keys
call and the definition of the attributes
param that is T::Hash[T.any(Symbol, String), Integer]
allowing the method to be called with a hash with keys, symbols, or strings.
Notice that because I defined the attributes
in that way (to accept both types of keys), I called symbolize_keys
to transform them into symbols so that I could slice
by ACCEPTED_KEYS
, which are symbols.
But there is another way to think about this:
Because the method has type definitions, there is no need to support both String and Symbol as keys for the hash. The developer who plans to use this method can see exactly what kind of objects the keys should be.
So, the method can be transformed into a simpler one where I remove the symbolize_keys
:
- sig { params(attributes: T::Hash[T.any(Symbol, String), Integer]).returns(T::Hash[Symbol, Integer]) }
+ sig { params(attributes: T::Hash[Symbol, Integer]).returns(T::Hash[Symbol, Integer])}
def accepted_attributes(attributes)
attributes
- .symbolize_keys
.slice(*ACCEPTED_KEYS)
end
An insight here is that Sorbet can help remove code that is there to handle the cases where the input is in various formats. Using types will provide hints to the user of the method about what kind of arguments are expected, so there is no need to handle multiple types.
Fixing the Sorbet splat arguments not well supported
Now, if you run the sorbet checker or play online on the sorbet playground, you will get an error that is something like this:
editor.rb:10: Splats are only supported where the size of the array
is known statically https://srb.help/7019
10 | attributes.slice(*ACCEPTED_KEYS)
And checking the Sorbet docs they say the following:
One solution would be to use T.unsafe
but I don't want to do that unless there is no other way around.
After trying a couple of things, I finally decided to refactor it to:
sig { params(attributes: T::Hash[Symbol, Integer]).returns(T::Hash[Symbol, Integer]) }
def accepted_attributes(attributes)
attributes.select { |key, _| ACCEPTED_KEYS.include?(key) }
end
And here comes the second insight:
The amazing thing about Ruby is that I can refactor that method from usingslice
to using select
and still remain explicit and read like an English sentence.
A more elegant solution
Another solution (thank you Ufuk Kayserilioglu for sending it to me) to fix the warning in Sorbet will be to define the type of the constant as array where I specify the type for each element:
- ACCEPTED_KEYS = T.let(%i[name username email].freeze, T::Array[Symbol])
+ ACCEPTED_KEYS = T.let(%i[name username email].freeze, [Symbol, Symbol, Symbol])
and so have the final code look like this:
ACCEPTED_KEYS = T.let(%i[name username email].freeze, [Symbol, Symbol, Symbol])
sig { params(attributes: T::Hash[Symbol, Integer]).returns(T::Hash[Symbol, Integer])}
def accepted_attributes(attributes)
attributes
.slice(*ACCEPTED_KEYS)
end
Also with recent changes in Sorbet it now knows how to infer the type of the constant without the need to explicitely specify it so the final code would be very simpler like this:
ACCEPTED_KEYS = %i[name username email].freeze
sig { params(attributes: T::Hash[Symbol, Integer]).returns(T::Hash[Symbol, Integer]) }
def accepted_attributes(attributes)
attributes.slice(*ACCEPTED_KEYS)
end
I think this is a much more elegant solution that makes the code simpler to read and keeps the original intention to use slice
Updates:
17 August 2024 - Added a new solution to fix the splat operator error from Sorbet provided by Ufuk Kayserilioglu
17 August 2024 - Updated the final example to use the latest Sorbet feature that knows to infer the type of the constants and so removes the need to use
T.let
Enjoyed this article?
๐ Subscribe to my Ruby and Ruby on rails courses over email at learn.shortruby.com- effortless learning anytime, anywhere
๐ Join my Short Ruby News newsletter for weekly Ruby updates from the community and visit rubyandrails.info, a directory with learning content about Ruby.
๐ค Let's connect on Ruby.social or Linkedin or Twitter where I post mainly about Ruby and Rails.
๐ฅ Follow me on my YouTube channel for short videos about Ruby
Subscribe to my newsletter
Read articles from Lucian Ghinda directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Lucian Ghinda
Lucian Ghinda
Senior Product Engineer, working in Ruby and Rails. Passionate about idea generation, creativity, and programming. I curate the Short Ruby Newsletter.