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

Static Extension Methods #41

Closed
lrhn opened this issue Oct 11, 2018 · 113 comments
Closed

Static Extension Methods #41

lrhn opened this issue Oct 11, 2018 · 113 comments
Assignees
Labels
extension-methods feature Proposed language feature that solves one or more problems
Milestone

Comments

@lrhn
Copy link
Member

lrhn commented Oct 11, 2018

UPDATE: Feature specification is available here:
https://github.com/dart-lang/language/blob/master/accepted/2.7/static-extension-methods/feature-specification.md

Original in-line proposal:

Possible solution strategy for #40. This issue doesn't (currently) have an concrete proposal.

Scoped static extension methods are successfully used in C# and Kotlin to add extra functionality to existing classes.

The idea is that a declaration introduces a static extension method in a scope. The extension method is declared with a name (and signature) and on a type, and any member access with that name (and signature) on something with a matching static type, will call the extension method.
Example (C# syntax):

class Container { // Because all methods in C# must be inside a class.
  public static String double(this String input)   // `this` marks it as an extension method on String
  { 
    return input + input; 
  }
}
...
   String x = "42";
   String y = x.double();  // "4242"

The method is a static method, all extension methods do is to provide a more practical way to invoke the method.

In Kotlin, the syntax is:

fun String String.double() {
    return this + this;
}

The String. in front of the name marks this as an extension method on String, and the body can access the receiver as this. Apart from these syntactic differences, the behavior is the same.

It's possible to declare multiple conflicting extension methods. Say, you add a floo extension method to both List and Queue, and then I write myQueueList.floo(). It's unclear which one to pick. In case of a conflict, the usual approach is to pick the one with the most specific receiver type (prefer the one on List over the one on Iterable), and if there is no most specific receiver type, it's a static error.
C# and Kotlin both allow overriding by signature, so there risk of conflict is lower than it would be in Dart, but the same reasoning can apply.

So, Dart should also have scoped extension methods. We can probably get away with a Kotlin-like syntax, for example:

String String.double() => this + this;

Extension methods can be used on any type, including function types and FutureOr (although probably not very practically on the latter). It can also be a generic function.
Example:

T List<T>.best<T>(bool preferFirst(T one, T other)) =>
    this.fold((T best, T other) => preferFirst(best, other) ? best : other);
...
List<int> l = ...;
print(l.best((a, b) => a > b));

In this case, the type argument should probably be inferred from the static type of the receiver (which it wouldn't be if the receiver was just treated like an extra argument).

Another option is to allow:

T List<var T>.best(bool preferFirst(T one, T other)) => ...

Here the method is not generic, so the type variable is bound by the list, and it will use the actual reified type argument at run-time (and the static type at compile-time, which can cause run-time errors as usual).
Example:

List<T> List<var T>.clone() => new List<T>.from(this);

Then anyList.clone() will create another list with the same run-time element type as anyList.
It effectively deconstructs the generic type, which nothing else in Dart currently does. The extension method gets access to the reified argument type, just as a proper member method does, which is likely to be necessary for some functionality to be implemented in a useful way.
(Type deconstruction is a kind of pattern-matching on types, it might make sense in other settings too, like if (x is List<var T>) ... use T ...).
This feature might not be possible, but if it is, it will be awesome 😄.

One issue with extension members is that they can conflict with instance members.
If someone declares T Iterable<T>.first => this.isEmpty ? null : super.first; as a way to avoid state errors, it should probably shadow the Iterable.first instance getter. The decision on whether to use the extension method is based entirely on the static type of the receiver, not whether that type already has a member with the same name.
Even in the case where you have a static extension method on Iterable for a member added in a subclass, say shuffle on List, an invocation of listExpression.shuffle() should still pick the static extension method, otherwise things are too unpredictable.

Another issue is that static extension methods cannot be invoked dynamically. Since dispatch is based on the static type, and you can't put members on dynamic (or can you? Probably too dangerous), there is nothing to match against.
Or would putting a static extension method on Object work for dynamic expressions?
If so, there is no way out of an extension method on Object, so perhaps casting to dynamic would be that out.

We say that static extension methods are available if the declaration is imported and in scope. It's not clear whether it makes any difference to import the declaring library with a prefix. There is no place to put the prefix, and Dart does not have a way to open name-spaces, which would otherwise make sense.

If we get non-nullable types, it might matter whether you declare int String.foo() => ... or int String?.foo() => .... The latter would allow this to be null, the former would throw a NSMError instead of calling when the receiver is null (and not a String). Until such time, we would have to pick one of the behaviors, likely throwing on null because it's the safest default.

We should support all kinds of instance members (so also getters/setters/operators). The syntax allows this:

int get String.count => this.runes.length;
void set String.count(int _) => throw 'Not really";
int String.operator<(String other) => this.compareTo(other);

We cannot define fields because the method is static, it doesn't live on the object.
We could allow field declarations to be shorthand for introducing an Expando, but expandos don't work on all types (not on String, int, double, bool, or null, because it would be a memory leak - mainly for String, int and double - the value never becomes unavailable because it can be recreated with the same identity by a literal).

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Oct 11, 2018
@lrhn lrhn self-assigned this Oct 11, 2018
@willlarche
Copy link

This would be my preferred solution to
dart-lang/sdk#34778 and #40 . Coming from Objective-C, it's second nature for me to add methods to classes (and instances, but, I'll take what I can get.)

It feels much more elegant than #42 too. My main desired use case is adding constructors to classes from other libraries that my library configures constantly.

@natebosch
Copy link
Member

Would it be possible to add an extension method on a type with a specified generic argument?

For example, could I add

num Iterable<num>.sum() => this.fold(0, (a,b) => a + b);

@zoechi
Copy link

zoechi commented Oct 16, 2018

I have troubles seeing the benefit of class Container { // Because all methods in C# must be inside a class.
They are not methods, they are static functions used in a way they look like methods, which means the class name is merely a namespace.

I expect code with #43 to be easier to read because it's more clear what's going on
in comparison to code where it looks like a method is called but it's actually something else.

@lrhn
Copy link
Member Author

lrhn commented Oct 16, 2018

I was definitely not advocating using the C# syntax, I was just showing as an example of an existing implementation (with the constraints enforced by that language).

Dart should definitely go with top-level syntax for extension methods, like in Kotlin, perhaps a syntax like the one I suggest later.

It's an interesting idea to add static methods to a class. There should be no problem with that:

static int List.countElements(List<Object> o) => o.length;
...
   List.countElements(someList) 
...

It's simpler than adding instance methods, you don't have to worry about dispatching on the static type of the object. You still have to worry about conflicts.

I'm not sure whether adding static methods to a class is really that useful. After all, it's longer than just countListElements as a top-level function. It does introduce a potentially useful context to the use. More interestingly, adding a factory constructor would then be a simple extension.
Since it has to be a factory, I guess the syntax could just be:

factory String.reversed(String other) => ...;

So, factory, static or nothing would designate the operations as factory, static or instance.
Seems possible.

As for specializing on type argument, that should also be possible.
The type to the left of the . is a type, not a class. It should work with any type, and the extension methods apply whenever the static type of the receiver of a member invocation is a subtype of the extension member receiver type. So, it can work, and probably should.
My only worry is that it's too convenient - we can't do something similar with normal instance members, so I fear people will start using extension members for that reason. We should consider whether we'd want generic specialization of classes, even if it's just syntactic sugar for a run-time check.

@munificent
Copy link
Member

Would it be possible to add an extension method on a type with a specified generic argument?

C# allows this and I have found it useful in practice, like the example you give. There are some interesting questions of how that interacts with subtyping. Is an extension method on Iterable<num> also available on Iterable<int>? Probably yes, but I'm not sure if that leads to confusing cases.

I'm not sure whether adding static methods to a class is really that useful.

It plays nice with auto-complete, which is one of the main way users discover APIs.

@willlarche
Copy link

willlarche commented Oct 17, 2018 via email

@yjbanov
Copy link

yjbanov commented Oct 17, 2018

This proposal could make Flutter's .of pattern auto-completable and probably more terse. E.g. you could auto-complete context. and get context.theme, context.localizations, and others as suggestions.

@marcguilera
Copy link

marcguilera commented Nov 13, 2018

I think this would be awesome to have for libs like RxDart or to make the of methods in Flutter more idiomatic.

I like the Kotlin syntax where you just do fun SomeClass.saySomething() = "say something". Probably the = should be changed with the => used in Dart but I think the approach overall makes sense.

Hopefully, with Dart going towards being a more strictly typed language, this is more possible than before? Are there any plans for this? I have seen it asked in a number threads.

@munificent
Copy link
Member

Hopefully, with Dart going towards being a more strictly typed language, this is more possible than before?

Yes, the shift to the new type system is absolutely what enables us to consider features like this now. They were basically off the table before.

Are there any plans for this? I have seen it asked in a number threads.

No concrete plans yet, but lots of discussion and ideas (which is what this issue is part of).

@MichaelRFairhurst
Copy link

MichaelRFairhurst commented Nov 15, 2018

Are static extension getters/setters planned to be supported as well?

Md5Sum String.get md5 => Md5Sum.from(this);

I'm not sure what a reasonable use case of an extension setter would be, unless you count this example:

void Stream<T>.set latest<T>(T value) => this.add(value);

myStream.latest = 4; // so you can add events via assignment?
myStream.latest = 6;

How would type inference work for generic extension methods? Would it be possible to write:

T T.get staticType<T> => T;

4.staticType; // int
(4 as num).staticType; // num

Not sure if this is good or bad. :)

I'm not entirely enthusiastic about adding deconstruction, also. For the sake of clearly being able to explain how the feature works, I think it's best to keep it a static concept, at least until there are other ways of doing runtime deconstruction in dart first. However I do see the issues in attempting to implement a function like List<T>.clone without it.

I'm also not stoked about the interaction with dynamic. I foresee this making json decoding worse when people inevitable add methods to Map, List, String, etc, and then use them in their unmarshalling code and get unexpected dynamic static values in the process....perhaps we should

  • match all possible extension methods with dynamic
  • report a compile-time error if more than one extension method matches
  • do a runtime check on the lookup in checked mode and report an error
  • launch this feature with a lint for extension method calls on dynamic values

I don't see a clear win/win here which concerns me.

Otherwise, I love it.

@passsy
Copy link

passsy commented Nov 18, 2018

Most Kotlin extension function for Iterable are inline functions. One cool feature about inlined functions are non-local returns. This case should be covered in the proposal when even reified functions are covered.

// Kotlin inline function (forEach) allows return of outer scope
fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // returns from hasZeros
    }
    return false
}
// Dart's forEach is not inlined and can't return outer scope
bool hasZeros(List<int> ints) {
    ints.forEach((i) {
		if (i == 0) return true; // forEach isn't inlined and can't return hasZeros here
	});
    return false; // always returns false
}

// for loops allow return but why should a for loop have more features than forEach?
bool hasZeros(List<int> ints) {
    for (var i in ints) {
		if (i == 0) return true; // returns from hasZeros
    }
    return false;
}

@munificent
Copy link
Member

Are static extension getters/setters planned to be supported as well?

Personally, I strongly feel that if we add extension methods, we should support all kinds of members: getters, setters, subscript, other operators.

In my C# days, I often missed the lack of extension getters.

alorenzen pushed a commit to angulardart/angular that referenced this issue Nov 28, 2018
Most of these correctly are emitted as expressions that are non-dynamic.

Notably:
  * Using collection literals (`[]`, `{}`) causes a dynamic call.
  * Using nested `*ngFor` causes a dynamic call.

There are probably other ways to (accidentally) cause a dynamic call that otherwise appears to be static. We should continue to add examples, as these would block the use of advanced Dart language features such as dart-lang/language#41 or dart-lang/sdk#35084 in the template.

PiperOrigin-RevId: 221693970
@leafpetersen
Copy link
Member

Most Kotlin extension function for Iterable are inline functions.

Is this really considered a good idea? It's largely a performance hack (basically a kind of macro), and it stops you from using actual functions as parameters. It's not that uncommon in Dart from what I've seen to pass named functions or tearoffs to forEach, which I think you can't do in Kotlin.

@passsy
Copy link

passsy commented Nov 28, 2018

Passing functions instead of a lambda is also supported in Kotlin. It also works for inlined functions.
Inlining is not only a performance "hack", it's the only way non-local returns and reified type parameters can be implemented. See https://kotlinlang.org/docs/reference/inline-functions.html

@leafpetersen
Copy link
Member

Ah, I see, it looks like you can use the ::foo syntax to pass a function.

@rrousselGit
Copy link

rrousselGit commented Nov 29, 2018

What about factory/static extension too?

factory MyClass.name() {
  return MyClass(42);
}

static void MyClass.anotherName() {

}

alorenzen pushed a commit to angulardart/angular that referenced this issue Nov 30, 2018
Most of these correctly are emitted as expressions that are non-dynamic.

Notably:
  * Using collection literals (`[]`, `{}`) causes a dynamic call.
  * Using nested `*ngFor` causes a dynamic call.

There are probably other ways to (accidentally) cause a dynamic call that otherwise appears to be static. We should continue to add examples, as these would block the use of advanced Dart language features such as dart-lang/language#41 or dart-lang/sdk#35084 in the template.

PiperOrigin-RevId: 221693970
alorenzen pushed a commit to angulardart/angular that referenced this issue Nov 30, 2018
Most of these correctly are emitted as expressions that are non-dynamic.

Notably:
  * Using collection literals (`[]`, `{}`) causes a dynamic call.
  * Using nested `*ngFor` causes a dynamic call.

There are probably other ways to (accidentally) cause a dynamic call that otherwise appears to be static. We should continue to add examples, as these would block the use of advanced Dart language features such as dart-lang/language#41 or dart-lang/sdk#35084 in the template.

PiperOrigin-RevId: 221693970
alorenzen pushed a commit to angulardart/angular that referenced this issue Nov 30, 2018
Most of these correctly are emitted as expressions that are non-dynamic.

Notably:
  * Using collection literals (`[]`, `{}`) causes a dynamic call.
  * Using nested `*ngFor` causes a dynamic call.

There are probably other ways to (accidentally) cause a dynamic call that otherwise appears to be static. We should continue to add examples, as these would block the use of advanced Dart language features such as dart-lang/language#41 or dart-lang/sdk#35084 in the template.

PiperOrigin-RevId: 221693970
@rrousselGit
Copy link

very curious if having this available in experimental mode is possible before 2020?

It's already available, on dart 2.6-dev

@renanyoy
Copy link

renanyoy commented Oct 19, 2019

when stable channel will hit 2.6 version it will be available in stable channel?
I use some plugins who doesn't like the 2.6-dev ;)

@renggli
Copy link

renggli commented Oct 20, 2019

I am having a lot of fun playing around with Dart 2.6.0-dev.8.0. The following example surprised me. Is this expected?

class Base {}

extension Extension on Base {
  static int method() => 42;
}

void main() {
  Extension.method();
  Base.method(); // Error: Method not found: 'Base.method'.
}

@eernstg
Copy link
Member

eernstg commented Oct 21, 2019

Yes, Base.method() is an attempt to call the static method in Extension as if it had been added to the target class Base, and there is no support for doing that. There was some discussion about supporting this, but it didn't get enough traction to actually happen.

@rrousselGit
Copy link

rrousselGit commented Oct 21, 2019

Wait, static extensions are not supported? I didn't even see the discussion. 😱

That's very sad. It allows pretty powerful things.
For example, it allows using hide to make a public static method, but not exposed outside of the package.

It's also very convenient with the static of pattern that Flutter uses.
We could have a model with no dependency on Flutter:

// model.dart
class Model {}

And, in the context of Flutter, add a static of method to it:

// another-file.dart
import 'package:flutter/widgets.dart';

extension on Model {
  static Model of(BuildContext context) {...}
}

Only extension static methods allows this.

@renanyoy
Copy link

renanyoy commented Oct 21, 2019

maybe he should try

class Base {}

extension on Base {
  static int method() => 42;
}

void main() {
  Base.method(); 
}

@slightfoot
Copy link

@renanyoy nice try. but that doesn't work.

@rrousselGit I agree with Remi here. I too needed a static extension method and found this exact problem also.

@passsy
Copy link

passsy commented Oct 21, 2019

@rrousselGit why not creating an extension on context instead of a static accessor?

class MyModel {
  final String name = "Hello";
}

extension MyModelExt on Context {
  MyModel get myModel => Provider.of<MyModel>(this);
}

class SomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(context.myModel.name);
  }
}

@slightfoot
Copy link

Another point is support for generics. When the class you are extending has generics to be able to use those in your extension.

extension IterableExtension on Iterable {
  List<E> toImmutableList<E>() => List.unmodifiable(this);
}

/// outputs:
///  List<dynamic>
///  List<int>
///  List<int>
void main() {
  final ex1 = [1, 2, 3].toImmutableList();
  print(ex1.runtimeType);

  final ex2 = [1, 2, 3].toImmutableList<int>();
  print(ex2.runtimeType);

  final ex3 = List<int>.unmodifiable([1, 2, 3]);
  print(ex3.runtimeType);
}

What I'd like is this.. but doesn't compile.

extension IterableExtension2 on Iterable<T> {
  List<T> toImmutableList() => List<T>.unmodifiable(this);
}

@rrousselGit
Copy link

@passsy That could work on small projects. But as the app grows, it's likely that we'll have two classes with the same name.

With an extension static method, then we can use the as/show/hide import directive.

import 'foo.dart' as foo;
import 'bar.dart' as bar;

foo.Model.of(context); // `.of` is a static extension on `Model`
bar.Model.of(context); // same here

Whereas with extension getters/methods we'll have a name clash.

It's also semantically better. That's how Flutter works. We do Theme.of(context), not context.theme.

@rrousselGit
Copy link

rrousselGit commented Oct 21, 2019

@slightfoot That's supported already:

extension IterableExtension<T> on Iterable<T> {
  List<T> toImmutableList() => List<T>.unmodifiable(this);
}

@slightfoot
Copy link

@slightfoot That's supported already:

extension IterableExtension<T> on Iterable<T> {
  List<T> toImmutableList() => List<T>.unmodifiable(this);
}

Doh!

@passsy
Copy link

passsy commented Oct 21, 2019

I bet the current "default" - using static methods Theme.of(context) - will soon change to context.theme. That's the whole point of extensions: Making object visible via auto-completion rather than knowing about all InheritedWidgets.

Name clashes are possible but you can also use as/show/hide for extensions. That's why they have a name.

// one.dart
extension One on String {
  void hello() => print("Hey");
}

extension Two on String {
  void hello() => print("Hello");
}
import 'one.dart' show One;
import 'one.dart' as two show Two;

void main() {
  "test".hello(); // Hey

  // name clash, fallback to static method
  two.Two("test").hello(); // Hello
}

@rrousselGit
Copy link

rrousselGit commented Oct 21, 2019

That's the whole point of extensions: Making object visible via auto-completion

The discussions weren't always about that.

There were a lot of mentions about "extensions allows adding any kind of member to a class", which includes static methods but also factory constructors (see #41 (comment))

extension on Foo {
  factory foo() { ... }
}

Although it's not supported either for some reason.

@renggli
Copy link

renggli commented Oct 21, 2019

Then I am wondering what is the benefit of supporting extension Extension on Base { static int method() => 42; }? This seems equivalent to class Extension { static int method() => 42; } in terms of calling method()?

More concrete: In my case I have various libraries that would benefit from static method/factory extensions. In each case I have abstract base classes with various static/factory-methods that help the users to instantiate sub-classes. I'd like to split up the existing code into multiple libraries as well as allow others to add their own extensions.

I don't think having the library users know the various extension classes is a viable solution. The most attractive workaround would be to introduce a separate factory object that can be extended and accessed from the base class, i.e. Base.factory.method(). However, this introduces some boilerplate, would change existing calling patterns, and looks quite unusual.

@aktxyz
Copy link

aktxyz commented Oct 21, 2019

this is pretty much how C# works, extension methods are instance methods, not static methods ... I have wanted true static extension methods many times ... but the bigger use case does seem to be instance methods ... maybe next go around on dart

@mit-mit
Copy link
Member

mit-mit commented Nov 14, 2019

Closing; this launched in preview in Dart 2.6. See the blog post:
https://medium.com/dartlang/dart2native-a76c815e6baf

@mit-mit mit-mit closed this as completed Nov 14, 2019
@mit-mit mit-mit added this to the Dart 2.6 milestone Nov 14, 2019
@mit-mit mit-mit moved this from Being implemented to Done in Language funnel Nov 14, 2019
@willlarche
Copy link

🎉

@kuhnroyal
Copy link

kuhnroyal commented Dec 9, 2019

Is there an issue tracking the autocomplete/import in the Flutter IntelliJ plugin? Because this is not working at all.

EDIT:
Nvm this is probably a problem with the IntelliJ Dart plugin.

@insinfo
Copy link

insinfo commented Apr 7, 2020

In the future, will it be possible to allow the addition of property with extension?

I have this use case, I currently use a custom list class implementation and I would like to use the extension to not need a custom class from the list just to add a simple property that allows to page results from the back end.

extension TotalRecords on List {
  int _totalRecords = 0;

  int get totalRecords => _totalRecords;

  set totalRecords(int totalRecords) {
    _totalRecords = totalRecords;
  }
}
import 'dart:collection';

class RList<E> extends ListBase<E> {
  final List<E> l = [];

  RList();

  @override
  set length(int newLength) {
    l.length = newLength;
  }

  @override
  int get length => l.length;

  @override
  E operator [](int index) => l[index];

  @override
  void operator []=(int index, E value) {
    l[index] = value;
  }

  int _totalRecords = 0;

  int get totalRecords => _totalRecords;

  set totalRecords(int totalRecords) {
    _totalRecords = totalRecords;
  }
}

I use this implementation in this package https://pub.dev/packages/essential_rest

@lrhn
Copy link
Member Author

lrhn commented Apr 8, 2020

Static extension methods cannot change the shape of the object, so it cannot add more storage slots.

What you can already do is to use an Expando.

extension TotalRecords on List {
  static final _totalRecords = Expando<int>();
  int get totalRecords => _totalRecords[this];
  set totalRecords(int value) {
    _totalRecords[this] = value;
  }
}

Expandos don't work on strings, numbers, booleans or null, but they should be fine for lists.

You won't be able to use extension methods to avoid the custom class, though. Since List already has an [] operator, you cannot override that with an extension method.

@maguro
Copy link

maguro commented Apr 11, 2020

@eernstg , re "Base.method()", what is the rational for not supporting it?

@lrhn
Copy link
Member Author

lrhn commented Apr 15, 2020

The rationale was that the affordances of the on type selection did not match well with acting like a real static method.

You can write extension on List<int>, but you cannot write List<int>.staticMethod, so it's unclear whether a static extension static method declared in that extension would apply to List.staticMethod, or to nothing.

An extension, as currently defined by the language, is defined on a type. A static method is defined on a class (or mixin) declaration. For generic classes (or mixins), those are very different things. Some types have no corresponding class at all, some do. There was no clear an easy way to extend the static instance member functionality to also add static members to types, and there was no clear and easy way to restrict the declaration to extensions on "classes" because there is no such thing.

@eernstg
Copy link
Member

eernstg commented Apr 18, 2020

Agreeing with @lrhn that a type and a namespace are very different things, I'd just add that a feature for scoped injection of declarations into namespaces (e.g., adding static methods, class variables, factory constructors to a class) should probably be a completely separate construct. It might turn out to be useful with classes, import prefixes, and other targets, and it might involve regular declarations as well as declaration aliases. So it's basically a whole new design process rather than a twist on static extension methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extension-methods feature Proposed language feature that solves one or more problems
Projects
Development

No branches or pull requests