Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add an "overrides" annotation #1647

Open
repomaa opened this issue Sep 29, 2015 · 36 comments
Open

add an "overrides" annotation #1647

repomaa opened this issue Sep 29, 2015 · 36 comments

Comments

@repomaa
Copy link
Contributor

repomaa commented Sep 29, 2015

this will prevent you from doing something like

class Foo
  def foobar
    2
  end
end

class Bar < Foo
  def foobaz # whoops a typo
    3
  end
end

Bar.new.foobar #=> 2 - Whaat?
@repomaa
Copy link
Contributor Author

repomaa commented Sep 29, 2015

also will enforce using same arg types and same return types or return types substitutable by the Liskov principle

@jhass
Copy link
Member

jhass commented Sep 29, 2015

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.

@repomaa
Copy link
Contributor Author

repomaa commented Sep 29, 2015

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

@repomaa
Copy link
Contributor Author

repomaa commented Sep 29, 2015

feel free to close if you think it best

@bcardiff
Copy link
Member

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)

@asterite
Copy link
Member

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.

@veelenga
Copy link
Contributor

I would like to see here override also. +1 for redef.

@sdogruyol
Copy link
Member

+1 for override

@ozra
Copy link
Contributor

ozra commented Oct 1, 2015

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:

  • Typos (not common), but also:
  • Involuntary overrides.

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.

@repomaa
Copy link
Contributor Author

repomaa commented Oct 1, 2015

+1 for raising when trying to override without redef

@oprypin
Copy link
Member

oprypin commented Oct 12, 2015

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)

@dimparf
Copy link

dimparf commented Oct 13, 2015

+1 redef or more usual override keyword.

@david50407
Copy link
Contributor

+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.

@007lva
Copy link

007lva commented Jun 17, 2016

+1 for redef

@zatherz
Copy link
Contributor

zatherz commented Dec 30, 2016

+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.

@mverzilli mverzilli added this to Issues in Subtyping Apr 30, 2017
@asoffa
Copy link

asoffa commented May 18, 2017

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).

@sevk
Copy link

sevk commented May 18, 2017

keep simple :)

@asoffa
Copy link

asoffa commented May 18, 2017

@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 :-)

@konovod
Copy link
Contributor

konovod commented May 18, 2017

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

  1. the sooner it is done is better (i hope it's not too late already)
  2. it would be nice to have 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.

@sevk
Copy link

sevk commented May 18, 2017

take 90% for syntax simple vs other language .
take 70% for performance vs other language .

@zatherz
Copy link
Contributor

zatherz commented May 18, 2017

@konovod crystal tool format already doubles as an updater in some cases.

@RX14
Copy link
Contributor

RX14 commented May 18, 2017

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.

@asoffa
Copy link

asoffa commented May 18, 2017

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?

@zatherz
Copy link
Contributor

zatherz commented May 18, 2017

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.

@asoffa
Copy link

asoffa commented May 19, 2017

@konovod To answer your question about the use cases of final, have a look at https://en.wikipedia.org/wiki/Final_(Java)

@konovod
Copy link
Contributor

konovod commented May 19, 2017

@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.

@asoffa
Copy link

asoffa commented May 19, 2017

@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 :-)

@konovod
Copy link
Contributor

konovod commented May 19, 2017

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.

@akadusei
Copy link

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

@jkthorne
Copy link
Contributor

I like the idea of it being opt in.
I like open classes and redefining methods on classes.

@asoffa
Copy link

asoffa commented Apr 17, 2019

In a spirit similar to #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.

@jan-zajic
Copy link
Contributor

+1 for annotations like in retired PR #6983 but with @[Reopen], i.e. @[Redefine], @[Override] and @[Reopen].

@HertzDevil
Copy link
Contributor

HertzDevil commented Jun 21, 2021

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.)
  • The block parameter restrictions of 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.)
  • Wrong method resolution when using default values #10231 must be resolved.
  • 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.

#1647 (comment) 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]).

@straight-shoota
Copy link
Member

@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.

@HertzDevil
Copy link
Contributor

HertzDevil commented Jul 21, 2021

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.

@straight-shoota
Copy link
Member

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 #14518 (comment) 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 #1647 (comment)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

Successfully merging a pull request may close this issue.