Detecting Class+ types

Hi all,

I want to work with some instances of a class Hero that is stored in an array:

class Hero
  def test
    puts the_type
  end
  
  macro the_type
    {{@type.id.stringify}}
  end
end

class Super < Hero
  def test
    puts "super test"
  end
end

me = [] of Hero
me << Hero.new
me.each(&.test)

outputs

Hero+

However I want to reference the Hero class TypeNode as the Hero+ TypeNode gives me an error when I try to extract a constant from it.

BUG: Hero+ has no types

My actual code is slightly more complex but really just wondering if there is a way to effectively resolve down to the nearest class from a Class+ TypeNode
Or is it possible to ensure that the array is strictly only storing Hero types and not any superclass types?

Possibly related to: https://github.com/crystal-lang/crystal/issues/5757

See Virtual and abstract types - Crystal

Could you not just make your Hero class abstract?

2 Likes

This works:

class Hero
  def test
    puts {{@type}}
  end
end

class Super < Hero
end

me = [] of Hero
me << Hero.new
me << Super.new
me.each(&.test)

When you use @type inside a method it automatically gets defined in each subtype separately to be able to resolve it differently.

Maybe it’s a bit confusing… something to improve.

No, this is not possible.

class Hero
  def test
    puts {{@type.id.stringify}}
  end
end

class Super < Hero
end

me = [] of Hero
me << Hero.new
me << Super.new
me.each(&.test)

outputs

Hero+
Super

Which I feel is unexpected as I understand that the contents of the array should be Hero+ however I’d expect the type inside the Hero class itself to be Hero

Which causes something like this to fail - however this works if you don’t use an array anywhere

class Hero
  CONST = "1234"
  
  def test
    puts {{([@type] + @type.ancestors.select { |c| c.has_constant?("CONST") }).map { |c| c.constant("CONST") } }}
  end
end

class Super < Hero
  CONST = "5678"
end

me = [] of Hero
me << Hero.new
me << Super.new
me.each(&.test)

outputs

Error: BUG: Hero+ has no types

whereas this

#me = [] of Hero
#me << Hero.new
#me << Super.new
#me.each(&.test)

Hero.new.test
Super.new.test

outputs

["1234"]
["5678", "1234"]

I see Hero and Super when I try that code locally and also in play: Carcin

Maybe also explain what you are trying to achieve? (the ultimate problem you want to solve). Maybe there’s a way to do it without macros at all.

The code in your example is different to mine, please see: https://play.crystal-lang.org/#/r/7hk8

I want to be able to collect the values of constants in an inheritance hierarchy - it’s not even really that much of an issue in practice, I just had a spec that was using an array and it didn’t compile due to the type being Hero+ and it seems like an issue.

I feel like these two bits of code should be equivalent

Ah, yes… Please report a bug. Thank you!

Actually, nevermind. The fix is really simple. I’ll push it later today.

1 Like

Here’s the fix: https://github.com/crystal-lang/crystal/pull/8149

3 Likes

Woot! Thanks @asterite