crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.34k stars 1.62k forks source link

add an "overrides" annotation #1647

Open repomaa opened 8 years ago

repomaa commented 8 years ago

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 commented 8 years ago

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

jhass commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

feel free to close if you think it best

bcardiff commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

sdogruyol commented 8 years ago

+1 for override

ozra commented 8 years ago

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.

repomaa commented 8 years ago

+1 for raising when trying to override without redef

oprypin commented 8 years ago

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 commented 8 years ago

+1 redef or more usual override keyword.

david50407 commented 8 years ago

+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 commented 8 years ago

+1 for redef

zatherz commented 7 years ago

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

asoffa commented 7 years ago

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 commented 7 years ago

keep simple :)

asoffa commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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

zatherz commented 7 years ago

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

RX14 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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

konovod commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 5 years ago

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 commented 5 years ago

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

asoffa commented 5 years ago

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.

jan-zajic commented 4 years ago

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

HertzDevil commented 3 years ago

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:

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

straight-shoota commented 3 years ago

@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 commented 3 years ago

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 commented 5 months ago

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