Search code examples
crystal-lang

Crystal: Ensure return value is not Nil


I have a helper class defined as follows:

require "toml"

module Test
  class Utils
    @@config

    def self.config
      if @@config.is_a?(Nil)
        raw_config = File.read("/usr/local/test/config.toml")
        @@config = TOML.parse(raw_config)
      end
      @@config
    end
  end
end

When I call this method elsewhere in the code:

server = TCPServer.new("localhost", Utils.config["port"])

I receive the following compile-time error:

in src/test/daemon.cr:10: undefined method '[]' for Nil (compile-time type is (Hash(String, TOML::Type) | Nil))

      server = TCPServer.new("localhost", Utils.config["port"])

There is no way for Utils.config to run something that is Nil, so I don't understand the error.

  1. How do I tell the compiler Utils.config will always return something that is not Nil?
  2. (Minor additional question) Is this a good design pattern for a resource (the config) that will be shared between classes, but only should be created once?

Solution

  • EDIT: See Johannes Müller's answer below, it is a better solution.


    In general, if you want to avoid Nil, you should type your class and instance variables:

    @@config : Hash(String,Toml::Type)

    That will help the compiler help you - by finding code paths that could lead to a Nil value and alerting you during compile time.

    A potential fix for the code:

    require "toml"
    
    module Test
      class Utils
        @@config = {} of String => TOML::Type # This line prevents union with Nil
    
        def self.config
          if @@config.empty?
            raw_config = File.read("/usr/local/test/config.toml")
            @@config = TOML.parse(raw_config)
          else
            @@config
          end
        end
    
      end
    end
    
    puts Test::Utils.config["port"]
    

    I couldn't test that directly due to the toml requirement, but a running example using strings instead is here: https://play.crystal-lang.org/#/r/30kl

    For your second question, this approach may work:

    require "toml"
    
    module Test
      class Utils
        CONFIG = TOML.parse(File.read("/usr/local/test/config.toml"))
      end
    end
    
    puts Test::Utils::CONFIG["port"]
    

    Example code using a string instead of TOML: https://play.crystal-lang.org/#/r/30kt