Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow generic-typed class methods/variables in abstract classes #8861

Open
postmodern opened this issue Feb 28, 2020 · 6 comments
Open

Allow generic-typed class methods/variables in abstract classes #8861

postmodern opened this issue Feb 28, 2020 · 6 comments

Comments

@postmodern
Copy link
Contributor

postmodern commented Feb 28, 2020

I ran into this limitation when attempting to define an abstract base class which accepted a generic type. While I somewhat understand the compiler needing to infer the type of the generic in order to generate appropriate code, however abstract classes should be treated as non-instantiatable code which sub-classes inherit from and implement the missing bits to make instantiatable. What struck me as odd about the error message, is it's coming from the scope of Foo (where the type of generic T should be known), but not Base (where the class variable @@foo is defined via the class_getter macro and given the type T?).

Example

abstract class Base(T)

  class_getter(foo : T?) { T.new(0) }

end

class Foo < Base(Int32)
end

p Foo.foo

Compiler Output

Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'class_getter'

Code in test.cr:3:3

 3 | class_getter(foo : T?) { T.new(0) }
     ^
Called macro defined in macro 'macro_140569709955712'

 1052 | macro class_getter(*names, &block)

Which expanded to:

 >  8 | 
 >  9 |           def self.foo
 > 10 |             if (value = @@foo).nil?
                                ^
Error: can't infer the type of class variable '@@foo' of Foo

The type of a class variable, if not declared explicitly with
`@@foo : Type`, is inferred from assignments to it across
the whole program.

The assignments must look like this:

  1. `@@foo = 1` (or other literals), inferred to the literal's type
  2. `@@foo = Type.new`, type is inferred to be Type
  3. `@@foo = Type.method`, where `method` has a return type
     annotation, type is inferred from it
  4. `@@foo = arg`, with 'arg' being a method argument with a
     type restriction 'Type', type is inferred to be Type
  5. `@@foo = arg`, with 'arg' being a method argument with a
     default value, type is inferred using rules 1, 2 and 3 from it
  6. `@@foo = uninitialized Type`, type is inferred to be Type
  7. `@@foo = LibSome.func`, and `LibSome` is a `lib`, type
     is inferred from that fun.
  8. `LibSome.func(out @@foo)`, and `LibSome` is a `lib`, type
     is inferred from that fun argument.

Other assignments have no effect on its type.

can't infer the type of class variable '@@foo' of Foo
@postmodern postmodern changed the title Allow generic-typed class methods in abstract classes Allow generic-typed class methods/variables in abstract classes Feb 28, 2020
@asterite
Copy link
Member

Generic types can't have generic class variables. I think there's a separate issue for that already.

@bcardiff
Copy link
Member

I couldn't find it either but essentially

Todays current behavior is

class Bar(T)
  class_property x : Int32 = 0
end

Bar.x = 1
Bar(Int32).x = 2
Bar(String).x = 3

pp! Bar.x         # => 3
pp! Bar(Int32).x  # => 3
pp! Bar(String).x # => 3

Instead of

pp! Bar.x         # => 1
pp! Bar(Int32).x  # => 2
pp! Bar(String).x # => 3

@da1nerd
Copy link

da1nerd commented May 7, 2020

I think this is the same issue that I'm experiencing. I'm trying to create a generic class variable.

Without Generics

What I'm trying to accomplish is to get around the fact that class variables do not persist between sub-classes.

So let's say for example that I have an Entity class with some internal counters. If I create an AnimalEntity sub-class, I have a new list of counters.

The solution is to use a singleton for the internal counter

class EntityCounter
  @@counter = 0
  # class methods for managing counter...
end

class Entity
  def dostuff
    EntityCounter.increment
    # do stuff
  end
end

Now the counter is the same for Entity and AnimalEntity.

With Generics

Here is a more complex example where I want to create a pool of resources by type. Each resource in the pool has a reference counter (sort of like a file reference count), so resources can be garbage collected once they have no more references.

I have several different types of resources that need different pools. Say for example a Texture, Model, and Shader.
Again, I'd like to be able to subclass any of those types, but still maintain the same resource pool for each.

Here is what I'd like to do:

class Reference(T)
  # instance vars and methods for managing reference
end
class ReferencePool(T)
  @@references  = Hash(String, Reference(T)).new
  # class methods for managing pool
end
class Texture

  def addstuff
     ReferencePool(Texture).add("texture-key", Texture.new)
  end
  def dostuff
     ReferencePool(Texture).use("texture-key")
  end
end
# etc...

Assuming the above code worked, I could easily create new types with their own pool of references. However, the compiler just complains that it can't infer the type for T.

@oprypin
Copy link
Member

oprypin commented May 7, 2020

I think it's a good use case.
For the record, C# can do it.

using System;
using System.Collections.Generic;

class C<T>
{
  static Dictionary<String, T> stuff = new Dictionary<String, T>();

  public static int foo() {
    return stuff.Count;
  }
}

class Application
{
  static void Main()
  {
    C<int>.foo();
  }
}

@bcardiff
Copy link
Member

bcardiff commented May 7, 2020

I would also prefer if class variables are scoped to instantiated generic class.
That also pushes class methods to be on instantiated generic class.

It is more powerful. If there is a need for shared class variables across all instances, there can be a helper non-generic class created for that end.

But we might need to wait. I doubt it will change soon.

@asterite
Copy link
Member

asterite commented May 7, 2020

I looked at the code and this might not be hard to implement... but I don't have the time right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Generics
Awaiting triage
Development

No branches or pull requests

5 participants