Open repomaa opened 8 years ago
also will enforce using same arg types and same return types or return types substitutable by the Liskov principle
I'm not sure, I find it utterly verbose for little benefit, such typos are not really common IME. If anything I'd like to see a keyword like redef
instead of def
, but that's harder to learn.
it is very common for major typed languages for a reason. But maybe you're right and this should be handled by some tool like rubocop
feel free to close if you think it best
A good thing of highlighting overrides is to detect code that requires attention if the base class changed. I'm not sure how it should play together between overrides, overloads, mixins, etc.
I like the idea of a redef as syntax. Maybe to do a check of raising an error if there is not match for the overload handled by that definition previously in the context... (just thinking out loud here)
I actually think some sort of override
annotation is a good thing. I also sometimes though I was redefining/overwriting a method and later found out I had a typo or forgot some argument.
I would like to see here override
also. +1 for redef
.
+1 for override
Haha, I was thinking about adding this issue just yesterday! Timing :) [ed: just saw that it was entered before that ;) ]
I like @jhass proposal of redef
- much terser than adding override
tag.
This catches two things:
Since Crystal can re-open types, if one uses a lot of 3rd part modules, chances are they will define a method you also define, and you haven't learned about. If you've defined it with def
(match your expectation), you'll get an error because it already exists. Otherwise you might end up affecting "upstream" functionality you didn't intend to.
+1 for raising when trying to override without redef
I support this.
With the following definition
override
keyword causes a compile time error if there is actually no previous def. (Nothing more, nothing less)
+1 redef or more usual override keyword.
+1 for override
, but I think it will only help programmers to check if the new method's return type and arguments are matching old one, it includes bigger arguments' type or smaller return type, like:
class Foo
def bar(x : Int32)
x > 2 ? "String" : 100
end
end
class Bar < Foo
override def bar(x : Int32 | String)
100
end
end
It's not the regular definition of Override
, but it makes more convenience and won't break the usage with other methods in parent class or somewhere.
+1 for redef
+1 for redef, but only for methods with the same argument arity. Return type changes should require redef
, but methods with different argument arity shouldn't.
Additionally, such a keyword (e.g. update
) could also be used for indicating that a module/class/struct is to be reopened:
module A
def say_hi
puts "hi"
end
end
update module A # `update` indicates that module `A` should already be defined and that we want
# to reopen it rather than create it from scratch
def say_hi # error: `say_hi` is already defined
puts "hello"
end
update def say_hi # ok
puts "hello"
end
end
module A # module `A` now shadowed rather than reopened, since no `update`
# (or this could be an error)
...
end
Then, if we want to enforce that something never change via reopening/extending/including/inheriting, a final
keyword could be used, e.g.
module A
final def say_hi
puts "hi"
end
end
update module A
update def say_hi # error: `say_hi` is `final`
puts "hello"
end
end
final module B
end
update module B # error: `B` is `final`
end
EDIT: Just to clarify, the use case of this for modules, etc. would be to prevent the following (analogous to the use case for methods):
module FooBarCheeseWiz
...
end
module FooBarCheezeWiz # oops
...
end
I am not advocating a ~
symbol per se - this was just the first thing that popped into my head. @[Reopen]
/@[Override]
annotations (for example) as suggested below could be used instead.
EDIT 2: I have changed ~
to a more transparent update
keyword — this proposed update
keyword would provide a check that an entity already exists (and with the same argument arity in the case of a function).
keep simple :)
@sevk "Simple" could be defined as not having to second-guess whether one is unintentionally monkey-patching or getting monkey-patched...or vice versa when it's desired :-)
I like the idea of overrides and I like redef
word.
For a module overrides, I like it too, but I think it's less important as chances to unintentionally patch or make typo in a module name are less.
I don't like a ~
symbol as it resembles cryptic C++ syntax - it doesn't have "intuitive" meaning, how are we even supposed to spell it? There are no legacy causes that forces to reuse every symbol on keyboard in every possible context instead of adding keywords.
About final
- maybe it's a nice idea (more possibilities to catch errors is better ), but i don't see actual usage for it. What modules\methods should never be overriden and why?
The problem I see with overrides is that it is a breaking change that breaks every piece of code written so far or converted from ruby later, so
crystal tool redef
or crystal tool unbreak
(for further occasions) that tries to compile code and replace def
to redef
where compilation error would be raised otherwise.take 90% for syntax simple vs other language . take 70% for performance vs other language .
@konovod crystal tool format
already doubles as an updater in some cases.
I strongly dislike the redef
syntax, as it seems inconsistent with the private def
and abstact def
syntaxes. Either override def
or using an @[Override]
annotation.
I also don't think that this should be a breaking change: the annotation should be completely optional and only add a check that you're overriding a method. This makes it useful when writing code to ensure you haven't typoed the method name, and when upgrading code to ensure that the method you're overriding hasn't changed name. I don't think that accidentally overriding methods is a problem that this proposal should attempt to solve.
I like the idea of having this be opt-in.
Perhaps also an optional @[Reopen]
annotation for modules/classes/structs to complement @[Override]
for methods?
If it's not going to be a breaking change and just an opt in check, it should IMO be an attribute and not a specifier like abstract
/private
.
@konovod To answer your question about the use cases of final
, have a look at https://en.wikipedia.org/wiki/Final_(Java)
@asoffa i know about final
in Java, and in Kotlin they even made all classes final by default. But Ruby\Crystal are much more dynamic, you can monkey-patch everything (and do it for e.g. a DSL), so forbidding to inherit from some classes\override some methods doesn't seem so useful.
As for optional attribute instead of required keyword, personally i don't like to see @[Override]
before all methods, override
looks much better, but it is opt-in and still better then nothing, so maybe attribute is a good solution.
@konovod Yes, my understanding is also that Crystal aims to be more dynamic, and there are always tradeoffs that must be made. Adding yet another keyword (e.g. final
) could indeed start running the risk of making the language more bloated than desired. Crystal does already have constants for the case of instances, so perhaps the case for final
is moot. Now that I think about, if a final
method were ever desperately needed, a constant Proc
could be used.
As for @[Override]
vs. override
it's a matter of taste, and it depends how often we expect the override feature to be used. If it's very often, perhaps the attribute option would indeed be too cluttering. For me personally there is no wrong answer :-)
I think there is no point to mark only few methods @[Override]
and override all others without it. You either use it for all overrides, or don't use at all (well, i can imagine some rules like "use it in actions\controllers, but don't use in a core engine", but that's imho pretty strange). At least if the point is to prevent typos and wrong parameter lists - I don't know where I'll make a next typo. So @[Override]
will be one more argument to don't use it at all. If redef
syntax is inconstistent, I'm for a override def
syntax.
That said, i agree that for an optional attribute @[Override]
is more logical, optional keyword looks strange, and of course having an @[Override]
is still better than don't having a feature at all.
@zatherz detection of overrides will require full compilation (or at least analysis of all included modules and stdlib), that is slower than just parsing, so i'm not sure that this function should be added to frequently used tool format
. Of course if an override is opt-in that doesn't matter and no tooling is needed.
I guess this is possible since #6063. Something like this should do:
class Object
annotation Override
end
macro inherited
macro method_added(method)
\{% if method.annotation(Override) && !@type.ancestors.any? &.methods.includes?(method) %}
\{% raise "Override error: '#{method.name}' does not exist in '{{@type.superclass}}'" %}
\{% end %}
end
end
end
# ===
abstract class GrandParent
def my_method
end
end
abstract class Parent < GrandParent
end
class Child < Parent
@[Override]
def non_existent # Override error: 'non_existent' does not exist in 'Parent'
end
@[Override]
def my_method # Good to go!
end
end
I like the idea of it being opt in. I like open classes and redefining methods on classes.
In a spirit similar to https://github.com/crystal-lang/crystal/pull/6983, I have changed my original proposal above to a (possibly optional) update
keyword used to check that a module / class / def (of the same argument arity) / variable already exists before opening / redefining it.
However, independent of the specifics, a key issue to consider is how this feature should play with include
and extend
in cases where some methods are to be redefined and some not. Something like update include
or override include
seems too unclear IMHO.
+1 for annotations like in retired PR #6983 but with @[Reopen], i.e. @[Redefine], @[Override] and @[Reopen].
If the intent of an @[Override]
annotation is to warn the user of potential typos, then the def's signature should match exactly some def from an inherited / included type, down to parameter names and named parameter order. But even defs that look the same might not override each other due to lookup rules:
class Bar
end
class Foo1
def foo(x : Bar)
1
end
end
class Foo2 < Foo1
class Bar
end
# does not override `Foo1#foo`!!
def foo(x : Bar)
2
end
end
Foo2.new.foo(Bar.new) # => 1
Thus if a
overrides b
, the ideal semantics of @[Override]
would be that:
a
and b
must have exactly the same AST nodes for their normal parameters (Def#args
), splat index (#splat_index
), double splat parameters (#double_splat
), block parameters (#block_arg
), and free variables, as these nodes make up the def signature.a.restriction_of?(b)
and b.restriction_of?(a)
must both be true; both defs are at least as specialized as the one other. (This implies a
yields if and only if b
yields, so that if neither has a block parameter, then neither can yield in the def body.)a
and b
, if present, must resolve to the same type. (This can be difficult to check if free variables are used in these restrictions.)a
must be defined in a proper subtype of b
. In particular Foo1
in the above example cannot be the same as Foo2
, or a sibling of it.https://github.com/crystal-lang/crystal/issues/1647#issuecomment-431525574 does not work because def equality compares also the def body, meaning the only defs that don't raise a compile-time error are verbatim copies (and as shown above, even these are inadequate for @[Override]
).
@HertzDevil I think this definition kind of makes, but I'm not really sure if it's worth going for.
You don't mention return type restrictions, but I suppose they would need to be an exact match? Or maybe the return type of b
could be further specialized.
The semantics are to tell the compiler that the annotated def's signature exactly matches a parent's def and thus effectively overrides it completely.
When there's only a single method overload in the parent, it's easy to reason about overriding it. But overloads add a factor of complexity. If there are multiple parent defs with similar signatures, @[Overrides]
matches the one with the exact same signature. But that might not be the overload that it's intended to override. Alternatively, the intention of an overriding def could also be to override all (or many) parent overloads (at least with a similar signature), to make sure no call can pass to a previous def.
Another practical use case is only partially overriding a parent def with more specific parameter type restrictions. @[Override]
wouldn't fit for that because b
would be more restricted. Yet, I think it would make equally make sense to express the intention of a partial override as the intention to do a full override (of a specific parent def).
So, this could certainly be useful, but it's also very limited to a use case that seems pretty narrow to me. Maybe it's important and widespread enough in the wild. Not sure. But as I currently see it, I'm not really happy about it.
That definition above would indeed be more suitable for @[Redefines]
.
In relation to #10904 I am thinking that if an implementation is annotated with @[Overrides]
, then it must override exactly one abstract def, and then the return type restriction could be inherited from that abstract def, with the exception that implicit conversions are not allowed:
abstract class Base
abstract def foo : Int32
abstract def foo(x) : Int
abstract def bar : Nil
def baz; end
end
class Derived < Base
@[Overrides] # okay, overrides `Base#foo`
def foo # return type must be `Int32` or a subtype of it
1
end
@[Overrides] # okay, overrides `Base#foo(x)`
def foo(x) # return type must be `Int` or a subtype of it
2_i64
end
@[Overrides] # error, cannot override more than one def
def foo(*x)
3
end
@[Overrides] # okay, overrides `Base#bar`
def bar # return type must be `Nil` or a subtype of it
4 # not allowed, unless return type restriction of `: Nil` is explicitly given
end
@[Overrides] # error, cannot override non-abstract def
def baz
5
end
end
This assumes that abstract defs themselves also override properly: (related to #9998)
abstract class Foo
abstract def foo : Int
end
abstract class Bar < Foo
@[Overrides] # okay, overrides `Foo#foo`
abstract def foo : Int32 # return type restriction is `Int` if not given
end
class Baz < Bar
@[Overrides] # okay, overrides `Bar#foo` but not `Foo#foo`
def foo # return type is `Int32`
1
end
end
@[Overrides]
itself is a very overloaded term (no pun intended), so @[Implements]
or even @[AbstractImpl]
might be more apt.
I think it would be useful to have an annotation to indicate that a def overrides the behaviour of a parent's implementation, simply for documentation purposes (see https://github.com/crystal-lang/crystal/issues/14518#issuecomment-2068854665 for example).
This is certainly a different use case than the implementation of an abstract method with exactly matching signature.
The overriding signature can be a subset of the parent def.
And I don't think there needs to be any specific compiler behaviour tied to this. It's sufficient to have this just as an informative annotation.
However, there could potentially be an error when there is no parent def (which should work more in a way like if super
would error, similar to the definition in https://github.com/crystal-lang/crystal/issues/1647#issuecomment-147411679).
this will prevent you from doing something like