Generics and instance vars

I might be missing some glaringly obvious solution, but anyway…

Crystal makes it quite easy to make a multiton:

class User
  @@users = {} of String => User
  def self.get(mail : String)
    @@users[mail] ||= User.new(mail)
  end
  def initialize(@mail : String); ... end
end

But combined with generics, the compiler won’t play ball.

class User(Handler)
  @@users = {} of String => User(Handler)
  def self.get(mail : String, handler : Handler.class)
    @@users[mail] ||= User.new(mail, handler)
  end
  def initialize(@mail : String, @handler : Handler); end
end

Error: can't infer the type parameter Handler for the generic class User(Handler). Please provide it explicitly

I think I understand why it’s complaining, but how would one go about making a multiton generic?

At the class level where @@users is defined, the generic type argument Handler is not resolved. So you cannot use it there. It’s also impossible to use the uninstantiated generic type (User).

A possible workaround is to introduce a dummy helper type (which is not generic) as parent of User and set that as value type for the hash:

module UserModule
end

class User(Handler)
  include UserModule
  @@users = {} of String => UserModule
  def self.get(mail : String, handler : Handler.class)
    @@users[mail] ||= User.new(mail, handler)
  end
  def initialize(@mail : String, @handler : Handler); end
end

I think it would be nice if String => User could work directly eventually. Not sure what exactly is missing to get that.

Not even really a “workaround” if one has other uses for the module.

Wouldn’t think it was much when the module thing works. Though I guess it presents some challenges to the compiler, as a type of Array doesn’t tell us what type Array#[] returns anymore.

But it would be mighty handy for decorator type generics where types with different type arguments still presents the same interface to the world.

Is there, or should there be an issue for this?

Personally I’d solve it by not using a class variable, but by pushing the collection of users to be an instance of something else. That also make it easier to test (as it make test independence easier as it would eliminate global-ish variable) and automatically solves the problem ‘well, what if I want a second list of users’.

1 Like

But that doesn’t exactly solve the problem. UserRepository would have the exact same problem of not being able to have an array of User but having to add the generic type. Of course, you could work around that by having multiple repositories, but then you’re playing hide and seek with your users, and what if the same user somehow shows up in both. Or, of obviously, being more creative with generics.

For testing, you just use User.new. No global state involved. And when testing the class interface, I see no problem in a User.clear method. It can even live in the spec file.

But, why would I? The point of the multiton is to make sure that there’s only ever one instance of User per user, to avoid having to deal with two parts of the application having a user with the same mail but different data. It’s a problem that doesn’t need solving.