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

An incremental way to add new class members #30811

Closed
matanlurey opened this issue Sep 20, 2017 · 7 comments
Closed

An incremental way to add new class members #30811

matanlurey opened this issue Sep 20, 2017 · 7 comments
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). customer-google3

Comments

@matanlurey
Copy link
Contributor

One of the frustrating parts, at least I imagine, of changing the Dart core libraries is that in strong-mode not implementing all interfaces/abstract classes' instance members is considered a build error. Imagine that someone wanted to add the whoop method to List:

abstract class List<T> implements ... {
  /// Returns the best element in the list.
  T whoop();
}

Now, every single class that either implements or extends List has a compile error.

One strategy would be creating a transitional mix-in:

abstract class ListWithWhoop<T> implements List<T> {
  /// Returns the best element in the list.
  T whoop();
}

... and adding it to every List implementation, and once a code-base is entirely "whoop-ified" (and assuming no new implementations of List are added), you can then try to add whoop to List. But wait, that's also an error in some build systems (i.e. Google's Bazel), because you omitted the @override annotation.

... so now you need to add @override to ListWithWhoop, and then cleanup and remove the ListWithWhoop class from the entire code base. I imagine for something like List or Iterable or anything else common with lots of pub packages/third party dependencies this will certainly take some time.

Proposal

Add an optional, @notImplemented (name is terrible, but accepting suggestions) annotation to dart:_internal for the entire purpose of instructing the frontend(s) to ignore this method, and pretend it doesn't even exist. For example, this would be valid, warning/error-free code:

abstract class List<T> implements ... {
  @notImplemented
  T whoop();
}

...

class BetterList<T> implements List<T> {
  // ... every method implemented but whoop() ...
}

An alternative could be to implement a default implementation, i.e. it would compile to:

class BetterList<T> implements List<T> { 
  // ... every other method implemented ...
  T whoop() => throw new UnsupportedError('Not implemented');
}

Once all classes that implement List implement whoop, then @notImplemented could be removed, and the implementation added to the base class (if it is abstract/meant to be extended or mixed in)

@lrhn
Copy link
Member

lrhn commented Sep 20, 2017

If I understand this correctly, we want a way to mark an instance member declaration as "auto-implement" so that every class inheriting the member in its interface, but not having an implementation for it yet, would be considered a warning instead of an error (and the error-recovery would add a matching member that throws when used, e.g., an UnimplementedError).

I can see the lure. It's not something you want to have around forever, but then, you should probably opt in to the feature to begin with (if you don't, it's still an error).

It's an interesting transition strategy. It allows part of an Opt-in, Opt-out, Mandatory transition chain (where Opt-in means implementing the member, Opt-out is when you remove the annotation and means you do have to explicitly implement the member to throw, and Mandatory just means that everybody else thinks you're broken if you don't implement it).

@vsmenon vsmenon added the area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). label Sep 25, 2017
@floitschG
Copy link
Contributor

A @notImplemented annotation would definitely be an option, but it has to hook much deeper into the language.
Either it is similar to default methods (like in Java interfaces), which is a full-fledged language change, or it allows abstract methods to be implemented (which is also a major language change wrt strong mode).

The willOverride annotation has the advantage that it is completely language independent. The override check is something that is not enforced by the language and only looked at by a linter. Modifying the linter is much easier.

@matanlurey
Copy link
Contributor Author

matanlurey commented Nov 14, 2017 via email

@matanlurey
Copy link
Contributor Author

I was going to close this, but if implemented properly (and behind a flag, so its not part of the spec), it would make it much much easier for non-stable releases to add temporary stub APIs without doing giant refactors.

@matanlurey matanlurey changed the title "In progress"-type annotation for adding instance members An incremental way to add new class members Aug 17, 2018
@matanlurey
Copy link
Contributor Author

I thought about this some more - what about a @stub or stub keyword?

abstract class Base {
  void a();
}

OK, 6 weeks later, I want to add b:

abstract class Base {
  @stub
  void b();
}

This would basically generate:

void b() => throw UnimplementedError();

... it could even just call super.noSuchMethod(), if you really wanted, similar to the forwarders.

@lrhn
Copy link
Member

lrhn commented Aug 17, 2018

The problem here is that annotations do not change the language. It's still a language error to not implement the interface, and no amount of annotations should be able to change that.
(That said, if it only works for the platform libraries, I guess we could find a way around it, by technically saying that we pre-process your input program, and it isn't the program we actually compile and run)

The proper solutions to this issue is either (or both of)

  • Interface default methods. An interface can declare a method implementation to be "default". All classes implementing the interface, and which do not have an implementation for that name already, will automatically "mix in" the default method. It's still breaking to have an incompatible member with the same name, but it's not a problem to not have anything. Default methods can only access the public interface of the class it's declared in.
  • Extension methods: Static methods declared on a class, selected by the static type of the expression they are called on. That would allow future.catchErrorOnly to be added without breaking any interfaces, because it's really just a static function with a fancy way of selecting it. It also means that the method cannot be intercepted in, say, DelegatingFuture. (There are multiple possible variants of extension methods, all of them would work here).

We could also get something like C#'s new/override distinction, which allows existing members with the same name to not conflict, because they are "new" (but that's tricky because we currently write nothing, and it means both "new" and "override").

@matanlurey
Copy link
Contributor Author

Closing in favor of #30416 then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). customer-google3
Projects
None yet
Development

No branches or pull requests

4 participants