how do you type instance variable caching in Sorbet?

703 views Asked by At

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 that nil does not type check with the Integer definition below. Also fair.
  • If I declare @expensive = [] in initialize, 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?

1

There are 1 answers

1
FullOnFlatWhite On BEST ANSWER

A Playground Link right back to you.

So, really using the initialize is the important part here.

  sig { void }
  def initialize
    @expensive = T.let(nil, T.nilable(T::Array[Integer]))
  end

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.