-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Redefine and Override annotations #6983
Add Redefine and Override annotations #6983
Conversation
I forgot to mention, but the usefulness of these annotations are:
|
f7289d9
to
6735f18
Compare
No idea why one of the CI failed:
|
I think I've seen that CI exception before... |
In my humble opinion case 2 of override is confusing and it will get mixed up with case 2 for redefine. I think that if someone had a really strong type restriction in ancestor, he will want to keep it for subtypes, not make it weaker. Also you will get a really nice rule about this kind of situations: tt won't work if you go from less restrictive type to more restrictive type. Grasping this for people will be a lot easier and language won't get complicated and will still feel very light. |
The thing is that case 2 is already being used in the compiler: class ASTNode
def restriction_of?(other : ASTNode, owner)
self == other
end
end
class Self < ASTNode
# I want to say that this overrides a case for the `restriction_of?` method and I want
# to make sure I didn't screw it up by having a typo or wrong number of arguments
@[Override]
def restriction_of?(type : Self, owner)
true
end
end That's the reason case 2 works like that: if I myself can't use it in the compiler what's the point? :-) |
I don鈥檛 like the syntax. Square brackets with at symbol look awful. It鈥檚 about annotations themselves, I guess. Happy for extra-verbose functionality, though! BTW, C# uses brackets only, without |
@vladfaust This PR isn't the place to discuss annotation syntax. That's been around for a long time. |
@asterite Our of curiosity, how does the |
I guess it depends on the number of annotations. I added a few and it was like 0.04s |
This might not be a reasonible case but what happens if these are combined? class Foo
def baz
end
end
class Bar < Foo
def baz
end
@[Overwrite]
def baz
end
@[Redefine]
def baz
end
end It could happen with open classes or something but I dont know if it is valid to check against. |
My understanding is that # ...
@[Override]
def baz
end
@[Redefine]
def baz
end
#...
That should probably depend on the order they're added? |
I see no problem with the above code. |
If I'd love to refactor Crystal::System to implement abstract core/stdlib methods, versus oneliner method delegations, virtual types and more methods that bloat the source, the AST, and that LLVM must inline. |
Is this going to be merged? I have a use-case of an advanced functionality shard which rewrites a function from a lesser functionality shard, and I have to make sure the former redefines the latter, i.e. there is a proper requiring order. |
I'd avoid adding features at this point without a proper design phase. |
Would such a design phase ever happen? |
Looks like I have a super-power of cancelling features just by saying that I need them 馃槄 |
Does having this type of features behind a flag can be an option? This will bring user feedbacks, and the work won't be lost in the deep closed PR abyss. |
@j8r I don't want to push a feature without a proper design phase, it doesn't matter if it's behind a feature flag. I pushed many features and stuff in the language without thoroughly thinking it and now I regret it (for example |
@asterite don't regret what you have done. The only thing is it would be better in the future to have edge features behind flags (like annotations, too late), to ease the modification/removal of them like you said, without breaking the "stable" API. |
Hello guys, i'm just a fan of crystal, but i have a suggestion. |
@LeandroRezendeCoutinho As far as I know, this feature would only be optional. It just hasn't been implemented yet because we need properly designed semantics first. That design process hasn't started. |
This PR introduces two annotations:
@[Redefine]
: when you redefine a method, that is, you replace an existing method with another one@[Override]
: when you define a method in a subtype that's present up in the type hierarchyYou can find examples in the specs in the diff.
The semantic is not that straightforward. Let's consider each annotation.
Redefine
I first named this
Overwrite
but I think it would be very confusing to haveOverride
andOverwrite
, andRedefine
is much clearer.This is this case:
The above example compiles fine: same method name, same arguments and same restrictions (none). But what if there are some type restrictions? We can have:
In case 1 it redefines because the previous method allowed an
Int32
as an argument, and now we are redefining that behaviour. We are not redefining it for all of the types, but if it happens for at least one of them then the annotation applies.(maybe later we can introduce a flag on the attribute to allow partial redefines like this or not, but I don't know if it's necessary)
In case 2 it doesn't redefine because if we pass an
Int32
, the compiler will still choose the previous definition.In case 3 the type restrictions are different so it doesn't redefine.
Override
This is this case:
The above example compiles fine: same method name, same arguments and same restrictions (none). But what if there are some type restrictions? We can have:
In case 1 it overrides: there's a stricter type restriction so if we invoke
Bar.new.foo(1)
it will call the method inBar
and not inFoo
.In case 2 it also overrides, unlike the
Redefine
case, because it's in a subtype: if we pass anInt32
toBar#foo
then it will consider that method (there's no multiple dispatch here).In case 3 the restrictions are different so it doesn't overwrite.
Caveats
Now for the caveats: I don't consider this implementation complete, but just because the whole language and all of its semantic implementation (or even definition) is incomplete. For example, right now we can redefine a method with a different argument name:
But if we invoked the first method like
foo(x: 1)
then now it won't compile... so in short, we are not taking names into account when redefining methods or when considering overloads. But this is a discussion to be held somewhere else.Then, return types are also not taken into account when overriding/redefining, so here it's fine to do that and change the return type, it will compile just fine. But that's also a discussion to be held somewhere else.
In the worst scenario, if you try to use these annotations and there's a bug or limitation in the compiler preventing you to use them, you just don't use them until these bugs/limitations are fixed. Nobody is forcing you to use them (but they are useful to be more explicit about what you are trying to do).
How it relates to docs
This PR doesn't modify the way docs are generates. I'd like to tackle that in a separate PR. Basically, if you redefine or override a method and don't provide additional doc comments, the doc generator should use the other method's docs. For this we'll have to store the "other" method somewhere, but doing so will make the compiler use more memory, so I'd like to find a better way to do that, maybe store these relationship outside of the AST nodes, so that the memory is only used when needed (I'm thinking a
Hash
could be enough). Also, we'll have to repeat the logic of finding the redefined/overridden method, so I'd like to refactor that too in a next PR.Fixes #1647