Open obskyr opened 6 years ago
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 haveParent
as a common type, isParent.class+
, that is,Parent
or 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.
The following would be a compilation error if #11190 is applied: (it does not matter whether Parent
itself is abstract)
abstract class Parent
end
class Foo < Parent
def self.foo; 1; end
end
class Bar < Parent
def self.foo; 2; end
end
(Foo || Bar).foo # Error: undefined method 'foo' for Parent.class (compile-time type is Parent+.class)
Rethinking about this issue, I don't think we need to do anything special here; as long as Parent
can possibly appear in a value of type Parent+.class
, it is natural to require an explicit definition of Parent.foo
. We could use a non-generic ClassMethods
module if we really want to exclude Parent
and enforce a distinct definition in the subclasses:
abstract class Parent
module ClassMethods
abstract def foo
end
macro inherited
extend ClassMethods
end
end
class Foo < Parent
def self.foo; 1; end
end
class Bar < Parent
def self.foo; 2; end
end
Now the following is an error:
(Foo || Bar).as(Parent::ClassMethods).foo # Error: can't cast Parent+.class to Parent::ClassMethods
But this works:
class A
property! x : Parent::ClassMethods
end
a = A.new
a.x = Foo
a.x.foo # => 1
a.x = Bar
a.x.foo # => 2
a.x = Parent # Error: no overload matches 'A#x=' with type Parent.class
([Foo, Bar] of Parent::ClassMethods).map(&.foo) # => [1, 2]
IMO this is good enough and we don't need to adjust the semantics of abstract defs to allow this to happen.
they useful there because classes are "first-class ctitizens" in Pascal
To me it is the opposite; class reference types there do not reuse the same semantics as regular classes. (Contrast with Ruby's singleton classes.)
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: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:...But that's runtime (and boilerplate-ey).
I hope there's something I'm missing!