Search code examples
crystal-lang

Initialize on first access


I have the following accessor method:

def self.database : DB::Database
  if @@database.nil?
    config = Utils.config["database"].as(Hash)
    connection = [
      "postgres://#{config["user"]}:#{config["password"]}",
      "@localhost/stats",
    ].join

    @@database = DB.open connection
  else
    @@database
  end
end

which is guaranteed to return a DB::Database. I am not sure how to declare the class variable:

class Daemon
  @@database

  def self.database : DB::Database
  end
end

I am presented with a few options by the compiler, but most options don't yield suggestions which compile:

  • @@database = uninitialized DB::Database compiles, but does not pass the nil? test as discussed in this GitHub issue.
  • There is no way for me to instantiate a placeholder DB::Database easily.
  • I'm also not sure how to initialize using the accessor function self.database, although that would work and give the type guarantees.

How can properly initialize the class variable? Any help would be greatly appreciated!


This solution works:

class Daemon
  @@database = uninitialized DB::Database

  def self.database : DB::Database
    config = Utils.config["database"].as(Hash)
    connection = [
      "postgres://#{config["user"]}:#{config["password"]}",
      "@localhost/stats",
    ].join

    DB.open connection
  end

The only problem with writing the class assessor this way is that I would start leaking file descriptors — each time I access @@database, I open a new connection to the database. I want to initialize @@database only during the first access and figure out a way to make the compiler happy with it starting out uninitialized.

Even more annoying:

class Daemon
  @@database = uninitialized DB::Database
  @@database_init = false

  def self.database : DB::Database
    if !@@database_init
      config = Utils.config["database"].as(Hash)
      connection = [
        "postgres://#{config["user"]}:#{config["password"]}",
        "@localhost/stats",
      ].join

      @@database = DB.open connection
      @@database_init = true
    end

    @@database
  end
end

Solution

  • You can make a class var nillable and conditionally assign a value in a class method:

    class Daemon
      @@database : DB::Database?
    
      def self.database
        @@database ||= begin
          config = Utils.config["database"].as(Hash)
          connection = [
            "postgres://#{config["user"]}:#{config["password"]}",
            "@localhost/stats",
          ].join
    
          DB.open connection
        end
      end
    end
    
    Daemon.database.query "..." # => #<PG::ResultSet:0x103ea2b40 ...>