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

Open
lrhn opened this Issue Oct 11, 2018 · 58 comments

Comments

Projects
None yet
@lrhn
Copy link
Member

lrhn commented Oct 11, 2018

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

@willlarche

This comment has been minimized.

Copy link

willlarche commented Oct 14, 2018

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

This comment has been minimized.

Copy link
Member

natebosch commented Oct 15, 2018

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

munificent commented Oct 16, 2018

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

This comment has been minimized.

Copy link

willlarche commented Oct 17, 2018

@yjbanov

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

munificent commented Nov 14, 2018

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

munificent commented Nov 28, 2018

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 added a commit to dart-lang/angular that referenced this issue Nov 28, 2018

misc(Goldens): Add some examples of template expressions.
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

This comment has been minimized.

Copy link
Member

leafpetersen commented Nov 28, 2018

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

leafpetersen commented Nov 29, 2018

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

@rrousselGit

This comment has been minimized.

Copy link

rrousselGit commented Nov 29, 2018

What about factory/static extension too?

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

static void MyClass.anotherName() {

}

alorenzen added a commit to dart-lang/angular that referenced this issue Nov 30, 2018

misc(Goldens): Add some examples of template expressions.
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 added a commit to dart-lang/angular that referenced this issue Nov 30, 2018

misc(Goldens): Add some examples of template expressions.
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 added a commit to dart-lang/angular that referenced this issue Nov 30, 2018

misc(Goldens): Add some examples of template expressions.
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

This comment has been minimized.

Copy link
Member

leafpetersen commented Dec 15, 2018

For me, the namespace wrapper is boilerplate, something dart usually tries to avoid.

Interesting - for me the Kotlin style feels really noisy and verbose. It's shorter for one extension, but as soon as you have more than one, having to repeat the name over the class you're extending over and over seems really annoying. Do you find it more common to only write single standalone extension methods? That's the only case I can see the Kotlin syntax being shorter. And I still like the grouping that the block syntax gives you, as well as the natural analogy to class syntax.

@yjbanov

This comment has been minimized.

Copy link

yjbanov commented Dec 15, 2018

Unrelated to the naming discussion, will access to generic type parameter from an extension allow "stealing" it? AFAICT this is not supported today:

List produceListOfSomethings() { ... }

var list = produceListOfSomething();
// no way to extract the generic type of the `list`

However, it seems now I can do this:

extension TypeBurglar<T> on List<T> {
  T stealType() => T;
}

var list = produceListOfSomething();
Type stolenType = list.stealType();

Not sure if there's anything wrong with allowing it.

@rrousselGit

This comment has been minimized.

Copy link

rrousselGit commented Dec 15, 2018

@yjbanov are you talking about this? :

Type _typeOf<T>(List<T> list) => T;


var list = produceListOfSomething();
Type stolenType = _typeOf(list);

Because this is something we can already do.

@passsy

This comment has been minimized.

Copy link

passsy commented Dec 15, 2018

Do you find it more common to only write single standalone extension methods?

I analyzed and categorized all extensions in my current project, 113 in total.

  • 57% (64) are standalone extensions. Meaning they would not share the same extension group with any other extension.
  • 47% (53) are file private scoped. Those would not result in import problems, they are only used within a single file. 7 of them were even defined inside a function.
  • 35% (40) are single line expressions.

By looking at my existing functions I found a few cases which would lead to problems with grouping into an extension object/thing:

Extensions for nullable types

In kotlin, with non-nullable types, extensions can be defined for T and T?. Defining extensions for both would result in two different extension groups in dart. (Assuming non-nullable types will be added to dart)

// dart
extension StringExt on String {
  String first5Chars() => this.take(5);
}

extension _InternalStringExt on String? {
  String? first7Chars() => this?.take(7);
}
// kotlin
fun String.first5Chars() = this.take(5)
private fun String?.first7Chars() = this?.take(7)

Add visibility to that problem and I can only very rarely see any extension group having multiple functions.

Extensions inside classes:

Inside a Presenter I found a extension for a external type (Disposable) which requires accses to internal properties of that class.

class Presenter {
    private rxhelper: RxHelper = RxHelper(this)

    protected fun Disposable.disposeInOnDestroy(): Disposable  {
        return rxHelper.manageDisposable(this) // extension body has access to class scope
    }  
}

This may be my misunderstanding, but the current proposal requests extension to be defined top-level, not inside classes. (Also very likely since dart doesn't support inner classes).

Extensions inside functions

I also found myself defining extensions inside a function. Dart supports functions inside functions. It should not prevent extension functions beeing defined inside functions. Example

@yjbanov

This comment has been minimized.

Copy link

yjbanov commented Dec 17, 2018

@rrousselGit

AFAICT functions will accept any generic argument as long as the argument is assignable. So for example, I can pass <Object> or <dynamic> because the reified List<String> is assignable to List<Object>. When omitted, Dart will not attempt to extract the reified type. For example, the following program prints dynamic despite the list being List<String>:

Type _typeOf<T>(List<T> list) => T;

main() {
  var list = produceListOfSomething();
  Type stolenType = _typeOf(list);
  print(stolenType);
}

produceListOfSomething() {
  return <String>[];
}
@lrhn

This comment has been minimized.

Copy link
Member Author

lrhn commented Dec 17, 2018

I would love if the extension method has access to the actual type arguments of the object.
There are potential extensions that won't work well without being able to know, e.g., the exact element type of the list (say, adding a splice function to List).
Can't guarantee that it's possible, but it's definitely desirable. Bob is correct that adding a name to the declaration gives us a place to put a type parameter:

extension SomeName<T> on List<T> { ... }

It's still a little magical that the T is bound to the actual type argument of the List when we match.

The _typeOf function indeed only reifies a static type, not the actual run-time type argument of the list.

@munificent

This comment has been minimized.

Copy link
Member

munificent commented Dec 18, 2018

Here's a concrete reason to use a surrounding declaration for extension methods instead of something like Kotlin's syntax:

void Function() Function().method() {}

Is this an extension method on functions returning functions, or an extension method on functions that returns a function? With a surrounding declaration, it's explicit:

extension A on void Function() Function() {
  method() {}
}

extension A on Function() {
  void Function() method() {}
}
@leafpetersen

This comment has been minimized.

Copy link
Member

leafpetersen commented Dec 18, 2018

I analyzed and categorized all extensions in my current project, 113 in total.

@passsy This is great, thanks for doing this! It's super useful to get concrete data. I've been thinking about extensions mostly as an API level affordance, but it sounds like you get a lot of value out of them as a way to define local helpers. Definitely something for us to keep in mind.

@rrousselGit

This comment has been minimized.

Copy link

rrousselGit commented Dec 20, 2018

Another potential syntax:

void function(String this, int parameter) {}

Since this is a reserved keyword it's not a breaking change.
The bonus point here is that we could use the function in 2 different ways:

"string".function(42);
function("string", 42);

It can be interesting for the functional side of Dart. It's not necessarily incompatible with the current extension Name on Type proposal either.
This is a syntax we will find in typescript for example.

@rrousselGit

This comment has been minimized.

Copy link

rrousselGit commented Dec 31, 2018

I'm not very convinced about adding static extension static members or constructors. You just write Class1.foo(x) instead of Class2.foo(x) or class1Foo(x) to access the static method.
I'd much rather keep the option of writing static helper functions inside the extension block, like:

I will disagree. I think this is a relatively important aspect of extensions when combined with something like code-generators.

Currently, code-generators requires to modify the class itself, which is far from ideal.
With extension constructors, we could make things such as json_serializable works on every classes, not just those defined by the user.

@Zhuinden

This comment has been minimized.

Copy link

Zhuinden commented Dec 31, 2018

Hmm it would be amazing if this could support extension functions over <T>, just like in Kotlin.

See this section on scoping functions if you're unfamiliar with what I'm talking about.

inline fun <R, T> T.let(transform: (T) -> R): R = transform(this) 

inline fun <R, T> T.run(transform: T.() -> R): R = transform(this)

inline fun <T> T.also(action: (T) -> Unit): T {
    action(this)
    return this
}

inline fun <T> T.apply(action: T.() -> Unit): T {
    action(this)
    return this
}

Then for example

class DogFragment: Fragment() {
    companion object {
        fun newInstance(dogId: String) = DogFragment().withArgs {
             putString("dogId", dogId)
        }
    }
}

Where withArgs is:

inline fun <T: Fragment> T.withArgs(argsBuilder: Bundle.() -> Unit): T = 
    this.apply {
        arguments = Bundle().apply(argsBuilder)
    }

Not sure how plausible it is for Dart, but I can guarantee that the ability to use .apply { and .let { can greatly reduce clutter (caused by having to declare a local variable that nobody is interested in, then having to talk to it multiple times, having to copy-paste the variable name over and over)

@munificent

This comment has been minimized.

Copy link
Member

munificent commented Jan 3, 2019

Another potential syntax:

void function(this: String, int parameter) {}

Since this is a reserved keyword it's not a breaking change.

True, but this is also already used inside parameter lists for initializing formals:

class Foo {
  String field;
  Foo(this.field); // <--
}

It's not ambiguous, but I think using this to mean two very different things would make it harder to read. Also, the syntax you propose doesn't gracefully extend to support extension getters, extension setters, extension operators, or generic extension methods on generic classes. For that last one, I mean things like:

extension ListStuff<S> on List<S> {
  foo<T>() { ... }
}

Here, I'm declaring an extension method that can be applied to lists of any type S. The method itself is also generic and takes a type argument T, which might be unrelated to S.

@passsy

This comment has been minimized.

Copy link

passsy commented Jan 3, 2019

@munificent I can't see why generic extensions or getters/setters couldn't be possible with the syntax proposed by @rrousselGit.
I slightly changed Remis syntax, because I think he accidentally used this: T instead of T this. It reminds me very much on how extensions work in Groovy.

Generic extensions

// Bob syntax
extension ListStuff<S> on List<S> {
  foo<T>() { ... }
}

// Remi syntax
void foo<S, T>(List<S> this, T param) { ... }

getter/setter extensions

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // normal getter/setter
  num get bottom => top + height;
  set bottom(num value) => top = value - height;
}

// Bob syntax
extension MyRectangleExtension on Rectangle {
  num get right => left + width;
  set right(num value) => left = value - width;
}

// Remi syntax
num get right(Rectangle this) => left + width;
set right(Rectangle this, num value) => left = value - width;

operator extensions

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  // normal operator
  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
}

// Bob syntax
extension MyVectorExtension on Vector {
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
}

// Remi syntax
Vector operator -(Vector this, Vector v) => Vector(x - v.x, y - v.y);

Remis solution has two advantages:

  • The extensions can be defined everywhere, even inside classes and functions. It's just a function with an argument following a convention.
  • I don't have to come up with a new name for every extension.
@rrousselGit

This comment has been minimized.

Copy link

rrousselGit commented Jan 3, 2019

The extensions can be defined everywhere, even inside classes and functions. It's just a function with an argument following a convention.

Oh, that's an interesting idea.

Which means we could have multiple times a method with the same "name" in a class?

class Foo {
  Vector v;
 
  bool operator ==(Vector this, Vector v) => true;

  // always return true because of the vector override above
  bool operator ==(Foo foo) => v == foo.v;
}

And that there's a notion of scope when accessing method/property?

num get bar(Vector this) => 42;

Vector v;
v.bar // 42

void function(Vector v) {
  num get bar(Vector this) => 0;

  v.bar // 0
}
@passsy

This comment has been minimized.

Copy link

passsy commented Jan 3, 2019

I thought about naming clashes as well. It shouldn't cause too many issues. Dart doesn't allow method overloading. Therefore it shouldn't be possible to define an extension which clashes with an existing member function or another extension with the same name.

num get bar(Vector this) => 42;

Vector v;
v.bar // 42

void function(Vector v) {
  num get bar(Vector this) => 0; // ERROR: `Vector.bar -> num` is already defined

  v.bar
}

But it should be possible to define extensions with the same name for the same type in two different scopes.

class Vector {
  final int x, y;
  Vector(this.x, this.y);
}

void functionA(Vector v) {
  num get bar(Vector this) => 0;

  v.bar // 0
}

void functionA(Vector v) {
  // no naming clash because it's the only Vector.bar function in scope
  num get bar(Vector this) => -1; 

  v.bar // -1
}
@rrousselGit

This comment has been minimized.

Copy link

rrousselGit commented Jan 3, 2019

I'm not sure about that one. Multiple extensions with the same name are not about method overload, but variable shadowing instead.

object.myExtension();

is just syntax sugar for:

myExtension(object)

With plain functions, the following is entirely valid:

num bar(Vector v) => 42;

Vector v;
bar(v); // 42

void function() {
  num bar(Vector v) => 0;
  
  bar(v); // 0
}

So I don't see any logical reasons for extensions to prevent such usage if the syntax allows it.

@leafpetersen

This comment has been minimized.

Copy link
Member

leafpetersen commented Jan 3, 2019

Hmm it would be amazing if this could support extension functions over <T>, just like in Kotlin.

This is a really interesting wrinkle. Basically it lets you be polymorphic over the receiver type and still use the "." syntax. Based on the test below, it looks like they resolve clashes in preference for the more specific extension.

inline fun <R, T> T.foo(transform: (T) -> R): R = transform(this) 
inline fun <R> String.foo(transform: (String) -> R) : R = transform("world")

fun start() {
    "hello".foo({print(it.toString() + "\n")})
    3.foo({print(it.toString())})
}

fun main(args: Array<String>) {
start()
}
@munificent

This comment has been minimized.

Copy link
Member

munificent commented Jan 3, 2019

void foo<S, T>(List<S> this, T param) { ... }

This still has some challenges around type inference. Consider a case like:

T firstOfType<S, T>(Iterable<T> this, S type) => this.firstWhere((e) => e is S);

In practice, users will want type inference to handle the first type argument since it's known from the type of the receiver. However, the second type argument can't (usually) be inferred and the user does expect to pass it explicitly. Dart doesn't have any mechanism for filling in "part" of a type argument list and leaving the rest inferred. We could add that, but it feels weird to me.

num get right(Rectangle this) => left + width;

Ah, interesting. Using get here avoids the ambiguity between an extension getter and a zero-argument extension method.

Overall, though, this syntax still feels strange to me. The way it is declared gets farther and farther from how it is used. Here, it looks like a getter has a parenthesized argument list, but you don't call it like that. In the generic example, it looks like it takes two type arguments when really a user should only ever explicitly pass one.

I just don't see a lot of value in making the receiver look like a parameter in the declaration when the entire intent of the feature is that it's not like an argument at the invocation site.

It interacts with namespacing in strange ways too. Consider:

foo(int a) {}
foo(String a) {} // Error. Name collision.

bar(int this) {}
bar(String this) {} // OK.

I think users will rightfully be surprised if a keyword inside the parameter list also changes how surrounding name is resolved.

The extensions can be defined everywhere, even inside classes and functions. It's just a function with an argument following a convention.

This is orthogonal. Whatever syntax we choose, we could choose to allow it inside the body of a function if we want.

@Zhuinden

This comment has been minimized.

Copy link

Zhuinden commented Jan 5, 2019

 num get right(Rectangle this) => left + width;

Or just do that Kotlin does,

num get Rectangle.right() => left + width;

Or what C# does

num get right(this Rectangle rect) => rect.left + rect.width;

@leafpetersen

This comment has been minimized.

Copy link
Member

leafpetersen commented Jan 9, 2019

T firstOfType<S, T>(Iterable this, S type) => this.firstWhere((e) => e is S);

I think the second parameter is pointless, but in any case, would you not want this to be

S firstOfType<S, T>(Iterable<T> this) => this.firstWhere((e) => e is S) as S;

in which case downwards inference would fill it in? In any case, I think this applies to Kotlin as well, and presumably it doesn't seem to be too much of a problem? I suppose the non-parametricity of Dart means that there are some use cases (such as the above) which don't come up in Kotlin though.

@munificent

This comment has been minimized.

Copy link
Member

munificent commented Jan 9, 2019

Thanks for fixing my mistakes. Yes, downwards inference will fill in the type sometimes, but not all the time:

S firstOfType<S, T>(Iterable<T> this) => this.firstWhere((e) => e is S) as S;

var list = [1, 2.2, 3];
var firstDouble = list.firstOfType<double, num>();

Here, there's no context type, so you have to write the type argument. That's fine, but it also forces you to write the other type argument too, because (as far as I know) we wouldn't support partially applying the type argument list and letting inference fill in the other parts.

@rrousselGit

This comment has been minimized.

Copy link

rrousselGit commented Jan 14, 2019

I've recently started using callable classes to have functions overload:

const someFunction = _SomeFunction();

class _SomeFunction {
  const _SomeFunction();

  void call() {}
  int customName() => 42;
}

which allows

someFunction();
int result = someFunction.customName();

I've been thinking, it would be cool to allow extensions to work on functions too:

void someFunction() {}

extension on someFunction {
  int customName() => 42;
}

This allows the same behavior as previously showcased, but as extensions.
In this example, there's obviously no this (unless used on methods), but the principle is the same.

This could be a solution to method overload from #145

@eernstg

This comment has been minimized.

Copy link
Member

eernstg commented Jan 14, 2019

I'm proposing 'scoped class extensions' (#177). The point is that 'static scoped extension methods' are just that, static, and it is useful to keep in mind that object-oriented dispatch (that is, instance method invocation whereby the chosen implementation is the one that is most specific for the dynamic type of the given receiver) is a useful mechanism, also for extensions. Scoped class extensions will add something to an existing class hierarchy which works very much like a regular instance method.

Scoped class extensions are compatible with static scoped extension methods: Both mechanisms can be given a similar syntax, and they are so different that it makes sense to have both.

In particular, static scoped extension methods make sense for receiver types that are function types or FutureOr, but scoped class extensions only support extensions of class types; static scoped extension methods can support more expressive type patterns (#170) for specifying which types of receivers they can be applied to, but scoped class extensions only admit a restricted form of type patterns, because they should offer the guarantee that the dynamic pattern match will always succeed whenever the statically known receiver type matches. And so on—the point is that these two mechanisms are quite different.

Yet, static scoped extension methods fall out as a special case of scoped class extensions: If only one class is extended, then the methods in a scoped class extension will be statically resolved.

So the point I'm making here is that we should keep in mind that the static scoped extension methods are similar to static methods, but that mechanism can very well coexist with a mechanism that is similar to instance methods, and I think it's useful to have both.

@leafpetersen

This comment has been minimized.

Copy link
Member

leafpetersen commented Jan 15, 2019

I filed an issue for discussion of the implications of the two approaches to dealing with generic parameter arguments discussed above (that is, allowing the extension to get the true runtime type parameters of a generic receiver as opposed to only the statically known approximation thereof).

@leafpetersen

This comment has been minimized.

Copy link
Member

leafpetersen commented Feb 1, 2019

Filed an issue for discussion of the interaction between short-circuiting null aware operators and extensions on nullable types here.

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