Inheritance of type of self (or new)

For my project, I need to use singleton in Crystal.
I have a working example that’s OK if the class, where the singleton is defined, is used.
But when I create a child class (I inherit from the previous class), the singleton instance is still typed as the parent class.

class Person
  def self.instance
    @@instance ||= new
  end
end

class John < Person
end

# typeof(John.instance) # => Person
p! typeof(John.instance)

How can I achieve a singleton pattern that’s consistent across inheritance (i.e. the correct output would be typeof(John.instance) # => John)?

Thanks

This seems to work:

class Person
  private class_property instance : self?
  
  def self.instance : self
    self.instance ||= self.new
  end
end

class John < Person
end

# typeof(John.instance) # => John
p! typeof(John.instance)

Try it on carc.in

1 Like

Right, the type of @@instance is Person and John is a Person. The type of a class variable is only one for the entire hierarchy, in this case it’s Person. But the runtime type of it is different.

Did you try printing the actual value?

https://carc.in/#/r/dr56

2 Likes

Okay, I played around with it a bit and discovered something interesting

The solution by @RespiteSage has the correct compile type indeed, but when accessing it, a stack overflow error is thrown (carc.in here). So that’s a no-go. (btw, why does that happen?)

So to be more exact, I am trying to do something like

class Person
  def self.instance
    @@instance ||= new
  end
end

class John < Person
  def hello
    p "Hello from John"
  end
end

class Greeter
  @@person : John = John.instance

  def self.greet
    @@person.hello
  end
end

Greeter.greet

This code above does NOT compile, because the compile type of instance is Person+ and not John. So even though @asterite is correct about the runtime type, it doesn’t really help in this scenario.

Thanks again :slight_smile:

Try this:

class Person
  def self.instance
    (@@instance ||= new).as(self)
  end
end

class John < Person
end

p! typeof(Person.instance)
p! typeof(John.instance)
1 Like

How hard could it be to add the as implicitly if the method has a type annotation?

class Person
  def self.instance : self
    @@instance ||= new # implicit: .as(self)
  end
end

This should go, of course, in every returned object of the body of a method. If the cast is invalid because of an error, that would be caught correctly. WDYT?

Edit: this has been discussed in the repo.

1 Like

Sorry for the late reply!

Thanks, everyone for the help provided, it was handy!

1 Like