I have code that looks like this (playground link):
# typed: strict
class A
extend T::Sig
sig { returns(T::Array[Integer]) }
def compute_expensive
[1, 2, 3]
end
sig { returns(T::Array[Integer]) }
def expensive
@expensive ||= T.let(compute_expensive, T::Array[Integer])
end
end
This fails to typecheck, saying that:
editor.rb:12: The instance variable @expensive must be declared inside initialize or declared nilable https://srb.help/5005
12 | @expensive ||= T.let(compute_expensive, Integer)
^^^^^^^^^^
I've tried a couple things to get around this…
- When I declare the type as
T.nilable(Integer)
, Sorbet says that the return type does not match the sig. Fair. - When I declare the type in
initialize
as@expensive = nil
, Sorbet says thatnil
does not type check with theInteger
definition below. Also fair. - If I declare
@expensive = []
ininitialize
, my assignment with||=
becomes unreachable. - I can of course say
@expensive = compute_expensive if @expensive.empty?
and then return@expensive
but I'm more interested in how Sorbet's type system can accommodate the||=
pattern.
This feels like a really common pattern in Ruby to me! How can I get Sorbet to type-check it for me?
A Playground Link right back to you.
So, really using the initialize is the important part here.
Because the memoization is still nil up until the point it's actually called, you have to allow for it to be nil, along with
T::Array[Integer]
, which then neccessitates for you to add it to the initialization to make the class sound.