Search code examples
rubysorbet

How would you do rose memoization with Sorbet?


Trying to annotate this code, the rose memoization (@||=) gives me an error Use of undeclared variable @git_sha.

# typed: strict
# frozen_string_literal: true

module Util
  extend T::Sig

  sig { returns(String) }
  def self.git_sha
    @git_sha ||= ENV.fetch(
      'GIT_REV',
      `git rev-parse --verify HEAD 2>&1`
    ).chomp
  end
end

As far as I've found, I should declare the variable's type with T.let but haven't figured out specifically how.


Solution

  • Sorbet now has built-in support for this, as of 0.5.10210. Before that, there were other workarounds (see below).

    1. Initialize the instance variable as T.nilable, and replace all direct access of the instance variable elsewhere with the method:

      # typed: strict
      # frozen_string_literal: true
      
      module Util
        extend T::Sig
      
        sig { returns(String) }
        def self.git_sha
          @git_sha ||= T.let(ENV.fetch(
            'GIT_REV',
            `git rev-parse --verify HEAD 2>&1`
          ).chomp, T.nilable(String))
        end
      end
      

      → View on sorbet.run

      This is the the preferred solution.

    2. Initialize the instance variable outside of the method, and give it a type annotation:

      # typed: strict
      # frozen_string_literal: true
      
      module Util
        extend T::Sig
      
        @git_sha = T.let(nil, T.nilable(String))
      
        sig { returns(String) }
        def self.git_sha
          @git_sha ||= ENV.fetch(
            'GIT_REV',
            `git rev-parse --verify HEAD 2>&1`
          ).chomp
        end
      end
      

      → View on sorbet.run

      Conceptually, there are two phases of execution for this class: when it's initialized, and when it's used. If an instance variable is not given a type annotation when it is initialized in Sorbet, it will be T.untyped everywhere (or an error in # typed: strict). Because if it's not annotated in the initialize, Sorbet can't know which code path might write into this location first. (Even in this case where there is one location, Sorbet doesn't do that sort of global analysis.)

      Sorbet only relaxes this when the instance variable is nilable, in which case it can be initialized anywhere, because Sorbet doesn't need to guarantee that it's initialized as non-nil.

    3. Use a different strictness level.

      Docs on strictness levels.

      If you find it too burdensome to add a type annotation, you can opt out of requiring a type annotation by using # typed: true, where the error requiring type annotations for instance variables is silenced.