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 usingselect
and still remain explicit and read like an English sentence.
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