Crystal: subclasses modify parent class constants

Created on 24 Jun 2020  Â·  15Comments  Â·  Source: crystal-lang/crystal

When creating a subclass from a parent class I want the constants to carry over to the sub classes. Right now if you mondify those classes the parents class constant gets modified.

class Foo
  CONST = [] of Foo.class

  macro inherited
    {{@type.name.split("::").first.id}}::CONST << {{@type.resolve}}
  end
end

class Bar < Foo
end

class Baz < Foo
end

p Bar::CONST
p Baz::CONST

output:

% crystal tmp.cr 
[Bar, Baz]
[Bar, Baz]

All 15 comments

I'm pretty sure this is the expected behavior since they're all just a reference to the same constant. I.e. same thing as if you did:

class Foo
  CONST = [] of Foo.class
end

class Bar < Foo
end

class Baz < Foo
end

Foo::CONST << Bar
Foo::CONST << Baz

Bar::CONST # => [Bar, Baz]
Baz::CONST # => [Bar, Baz]

Yes, this is expected behavior. You can declare the constant again in the subclasses... but maybe explain what you want to accomplish in general?

I want to create a class per DB table and what them to have a constant array of the columns they have.

class TableOne < Table
  COLUMNS = [Email, Id]
end

Class TableTwo < Table
  COLUMNS = [Email, Id
end

I would rather be able to discover these through a macro.

Whats the reasoning for using a constant for this? Won't you also need something to allow for getting/setting the values related to each column?

Could do something like:

annotation Column; end
class TableOne
  @[Column]
  getter! id : Int32

  @[Column]
  property! email : String
end

Then you can access the related ivars/annotation data at compile time via macros.

I’ve run into something similar in a number of spots in Lucky. The use case is that it needs to be a constant so that I can use the data in a macro. As far as I know constants are the only way to get at the information. What we end up doing is copying the constants when inherited and included.

https://github.com/luckyframework/lucky/blob/master/src/lucky/exposable.cr and https://github.com/luckyframework/lucky/blob/master/src/lucky/assignable.cr

I can see why constants are the same, but it’d be awesome if maybe there was a built in crystal method to “copy” the constants. So I could call “copy_constant EXPOSURES” and it’s copy the constant when including and inheriting.

The use case is that it needs to be a constant so that I can use the data in a macro. As far as I know constants are the only way to get at the information.

That's one of two ways. The other being annotations. However each has their pros and cons. Depends on the context which one would be better.

I think the current approach of using constants to store compile-time data, together with inherited/included hooks is very messy. For 2.0, or even during the development of 1.0, we should gather some use cases and think of how to solve this without so many hoops.

@asterite That is a fantastic idea. Maybe some other structure for storing compile time data that isn't a constant would be super cool. Ping me if you want to work on this or need some use-cases. I've got a few in Lucky and Avram :D

You could do this:

class Foo

  macro inherited
    CONST = [] of Foo.class
    {{@type.name.split("::").first.id}}::CONST << {{@type.resolve}}
  end
end

class Bar < Foo
end

class Baz < Foo
end

p Bar::CONST
p Baz::CONST

Unless you're expecting to use Foo::CONST directly.

This would not be necessary for me if it was easier to query left an right in namespace nesting. This would be an awesome feature for crystal.

Foo::Bar::Baz

I would like to access all the module/classes right of Bar and the module/class left of Bar.

With a feature like this I would not have to create all the constants.

@jwoertink You solution is really cleaver and I like it.
I think I am finding a way to work with this pattern but I have found a frustrating little quirk.

class Foo
  macro inherited
    CONST = [] of Foo.class
    {{@type.name.split("::").first.id}}::CONST << {{@type.resolve}}
  end

  def self.const
    CONST
  end
end

class Bar < Foo
end

class Baz < Foo
end

p Bar.const

output:

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

In tmp.cr:9:5

 9 | CONST
     ^
Error: undefined constant CONST

You can work around the lookup problem by moving everything to the inherited macro. However it would be nice to have this be able to lookup the constant from the generated code.

@wontruefree This works:

class Foo
  macro inherited
    CONST = [] of Foo.class
    {{@type.name.split("::").first.id}}::CONST << {{@type.resolve}}

    def self.const
      CONST
    end
  end
end

class Bar < Foo
end

class Baz < Foo
end

p Bar.const

That said, I'm really curious about why all of this is needed...

This works too:

class Foo
  macro inherited
    CONST = [] of Foo.class
    {{@type.name.split("::").first.id}}::CONST << {{@type.resolve}}
  end

  def self.const
    {% begin %}
      {{@type}}::CONST
    {% end %}
  end
end

class Bar < Foo
end

class Baz < Foo
end

p Bar.const

Thanks that seems to work for some things.

I dont know why but this pattern does not work in a few places for me. I have not narrowed down why yet.

I'm closing this because this is expected behavior.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

costajob picture costajob  Â·  3Comments

asterite picture asterite  Â·  3Comments

oprypin picture oprypin  Â·  3Comments

Sija picture Sija  Â·  3Comments

relonger picture relonger  Â·  3Comments