At the moment, there seems to be no way to abstract def class methods. This is a bit of a problem, in case you want to guarantee subclasses implement certain constructors! Take this for example:
abstract class Thing
abstract def self.parse(s : String) : Thing
abstract def self.construct_somehow(a : Int, b : String, c : Bool) : Thing
end
Right now, that doesn't work, but instead errors out with the error can't define abstract def on metaclass. The best way I can come up with to do something similar in the current version of Crystal is this:
abstract class Thing
abstract def self.parse(s : String) : Thing
raise NotImplementedError.new # Or similar
new
end
abstract def self.construct_somehow(a : Int, b : String, c : Bool) : Thing
raise NotImplementedError.new
new
end
end
...But that's runtime (and boilerplate-ey).
I hope there's something I'm missing!
Note that because of abstract class Thing, Thing is abstract so it can't be instantiated:
Thing.new # compile error
However, classes are already "instantiated":
Thing # => Thing
So, abstract methods in abstract types make sense, because they must be defined in concrete subclasses that can be instantiated. That doesn't apply to classes because you can use them without instantiating them.
So in my mind it doesn't make sense to have abstract class methods, and that's why they never got implemented. And in every other statically typed language there's no such thing either.
However, there is very much a legitimate use case for when you'd like subclasses to have the same set of constructors!
Sorry, I don't think this can (or will) be implemented.
FWIW there are other use cases for requiring children to implement a class method other than instantiation.
For example it would be nice if I could have did
abstract struct Athena::EventDispatcher::Listener
abstract def self.subscribed_events : AED::SubscribedEvents
...
end
And in every other statically typed language there's no such thing either.
I know i'm slightly late, but there is such thing in Pascal\Delphi. https://ideone.com/qcrPSw
But they useful there because classes are "first-class ctitizens" in Pascal, so you can do
if random < 0.5 then c := Child1 else c := Child2; x := c.Create;
Rust's interface system, traits, can require implementors to provide static methods:
// abstract struct Foo
trait Foo {
// abstract def self.bar
fn bar();
// abstract def baz
fn baz(&self);
}
struct A { val: i32 }
// struct A< Foo
impl Foo for A {
fn bar() {
println!("bar");
}
fn baz(&self) {
println!("baz: {}", self.val);
}
}
fn main() {
A::bar(); // print "bar"
let a = A { val: 5 };
a.baz(); // print "baz: 5"
}
This being missing is what prevents the concept of From being convenient in #8511 , which has a similar motivation to this issue
I think we could implement something like this, but if you misuse it you will still get a runtime error.
What I mean is, if you have:
abstract class Parent
abstract def self.foo
end
class Foo < Parent
def self.foo; 1; end
end
class Bar < Parent
def self.foo; 2; end
end
If you do:
(Foo || Bar).foo
then the type of Foo || Bar, because they have Parent as a common type, is Parent.class+, that is, Parent or any of its subclasses. In this case the above code will work, but you could also have this:
(Parent || Foo || Bar).foo
In this case the type of the parenthesized expression is the same: Parent.class+. But the expressions' result is Parent and foo isn't defined for it (it's abstract), so I guess we can make it raise at runtime saying "foo is abstract for Parent".
This is still a bit better than the current state because it forces you to implement those abstract methods, and there's a very little chance that you'll end up misusing them.
Thoughts?
That sounds fine to me; at least, it makes sense from a user's perspective. I suppose this is somewhat consistent with other runtime type operations, such as
expr = "1" || 2
expr.as(Int32) # Unhandled exception: cast from String to Int32 failed
Although I'm curious about the compiler's understanding of that code; my impression is that the compiler isn't able to semantically block expressions like that containing abstract metaclasses, before they are cast to Parent.class+?
I can't quite imagine a use case that currently exists for handling Parent.class itself at runtime off the top of my head that would prevent making that a compiler error reasonable.
I see that (Parent || Foo || Bar).new.foo is, understandably, already a segfault: https://carc.in/#/r/85fj. This would presumably fix this as well?
Yes, that's already tracked here: https://github.com/crystal-lang/crystal/issues/3835
I once tried to fix it thinking it was easy but it's not. Maybe some day...
For reference, class methods are required on Serializable in the stdlib:
converter: specify an alternate type for parsing and generation. The converter must define from_json(JSON::PullParser) and to_json(value, JSON::Builder) as class methods.
In this case, a Converter module with abstract class method will be useful.
You can, in fact, do this already somehow:
abstract class Thing
macro inherited
extend ClassMethods(self)
end
private module ClassMethods(D)
abstract def parse(s : String) : D
abstract def construct_somehow(a : Int, b : String, c : Bool) : D
end
end
class ThingImpl < Thing
def self.parse(s : String) : ThingImpl
# ...
end
def self.construct_somehow(a : Int, b : String, c : Bool) : ThingImpl
# ...
end
end
If .parse isn't defined you get abstract `def Thing::ClassMethods(D)#parse(s : String)` must be implemented by ThingImpl.class, or if it returns a Thing instead of ThingImpl, you get this method must return ThingImpl, which is the return type of the overridden method Thing::ClassMethods(D)#parse(s : String), or a subtype of it, not Thing.
This trick relies on the fact that T.f is mostly equivalent to T.class#f in Crystal; making Thing::ClassMethods private and not actually defining these methods in Thing.class itself means there should be fewer chances of misuse.
Likewise, for the converter case which does not require actual classes, one could write this:
module JSONConverter(T)
abstract def from_json(pull : JSON::PullParser) : T
abstract def to_json(value : T, json : JSON::Builder)
end
module Time::EpochConverter
extend JSONConverter(Time)
def self.from_json(pull : JSON::PullParser) : Time
Time.unix(pull.read_int)
end
def self.to_json(value : Time, json : JSON::Builder)
json.number(value.to_unix)
end
end
{% Time::EpochConverter.class < JSONConverter(Time) %} # => true
No macro hooks are required here.
What I mean is, if you have:
abstract class Parent abstract def self.foo end class Foo < Parent def self.foo; 1; end end class Bar < Parent def self.foo; 2; end end[...] then the type of
Foo || Bar, because they haveParentas a common type, isParent.class+, that is,Parentor any of its subclasses.
I don't think Parent.class should be abstract; the mere fact you can pass Parent around means it is possible to create instances of Parent.class (even though the user doesn't get to call .new), so Parent.class is not an abstract base metaclass. If we interpret metaclasses as such and define abstract class methods in terms of the above snippets, (Foo || Bar).foo just works, whereas (Parent || Foo || Bar).foo should be a compile-time error, because the receiver's type is now Parent+.class instead of Parent.class+, and Parent.foo doesn't exist as Parent doesn't extend ClassMethods(Parent) itself.
Most helpful comment
I think we could implement something like this, but if you misuse it you will still get a runtime error.
What I mean is, if you have:
If you do:
then the type of
Foo || Bar, because they haveParentas a common type, isParent.class+, that is,Parentor any of its subclasses. In this case the above code will work, but you could also have this:In this case the type of the parenthesized expression is the same:
Parent.class+. But the expressions' result isParentandfooisn't defined for it (it's abstract), so I guess we can make it raise at runtime saying "foois abstract for Parent".This is still a bit better than the current state because it forces you to implement those abstract methods, and there's a very little chance that you'll end up misusing them.
Thoughts?