Coder Social home page Coder Social logo

Comments (115)

matanlurey avatar matanlurey commented on May 29, 2024 42

@lrhn:

If you could overload methods, tear-offs would no longer work.

You already cannot tear off what users write instead of overloads, which is multiple methods:

class Foo {
  void bar() {}
  void barString(String s) {}
  void barNumber(num n) {}
}

... so given that overloads would be sugar for that, I don't see it any worse.

@eernstg:

and it is an important part of the Dart semantics that we have enough information available at run time to actually make that happen safely

Is it being dynamically invokable a requirement? I don't think it is.

I'd heavily like to see a push for overloads in the not-so-distance future. My 2-cents:

(@yjbanov and @srawlins get credit for parts of this discussion, we chatted in person)

Proposal

Don't allow dynamic invocation of overloaded methods

... or limit how they work:

class Foo {
  void bar() => print('bar()');
  void bar(String name) => print('bar($name)');
  void bar(int number) => print('bar($number)');
}

void main() {
  dynamic foo = new Foo();

  // OK
  foo.bar();

  // Runtime error: Ambiguous dispatch. 2 or more implementations of `bar` exist.
  foo.bar('Hello');
}

If you wanted to be real fancy (@munificent's idea, I think), you could have this generate a method that does dynamic dispatch under the scenes. I'm not convinced this holds its weight (and makes overloading, which should be a great static optimization a potential de-opt), but it's an idea.

I realize this adds a feature that is mostly unusable with dynamic dispatch, but Dart2 already has this issue with stuff like reified generics.

Consider this very common bug:

var x = ['Hello'];
dynamic y = x;

// Error: Iterable<dynamic> is not an Iterable<String>
Iterable<String> z = y.map((i) => i);

Limit tear-offs if the context is unknown

Rather, if the context is ambiguous, then make it a static error.

void main() {
  var foo = new Foo();

  // Static error: Ambiguous overload.
  var bar = foo.bar;

  // OK
  var bar = (String name) => foo.bar(name);

  // Also OK, maybe?
  void Function(String) bar = foo.bar;
}

... another option is have var bar = foo.bar basically generate a forwarding closure (a similar de-opt to the dynamic dispatch issue). Again, not my favorite, but I guess no more bad than code already being written.

Side notes

Let's consider how users are working around this today:

  1. Using Object or dynamic with is checks and optional arguments:
class Foo {
  void bar([dynamic nameOrNumber]) {
    if (nameOrNumber == null) {
      print('bar()');
      return;
    }
    if (nameOrNumber is String) {
      // ...
      return;
    }
    if (nameOrNumber is num) {
     // ...
     return;
    }
  }
}
  • This works with dynamic dispatch
  • This works with tear-offs
  • This isn't very efficient, and it's very hard/impossible to create complex overloads
  • You lose virtually all static typing
  1. Creating separate methods or constructors:
class Foo {
  void bar() {}
  void barString(String s) {}
  void barNumber(num n) {}
}
  • This doesn't work with dynamic dispatch
  • This doesn't work with tear-offs
  • This is the most efficient, but cumbersome and creates a heavy API surface
  • Best static typing

I think the idea for overloads is no worse than 2, and you can still write 1 if you want.

EDIT: As @srawlins pointed out to be, another huge advantage of overloads over the "dynamic"-ish method with is checks is the ability to have conditional type arguments - that is, type arguments that only exist in a particular context:

class Foo {
  void bar();
  T bar<T>(T i) => ...
  List<T> bar<T>(List<T> i) => ...
  Map<K, V> bar<K, V>(Map<K, V> m) => ...
}

It's not possible to express this pattern in dynamic dispatch (or with a single bar at all).

from language.

matanlurey avatar matanlurey commented on May 29, 2024 24

By the way, this would have solved the Future.catchError issue:

class Future<T> {
  Future<T> catchError(Object error) {}
  Future<T> catchError(Object error, StackTrace trace) {}
}

... as a bonus :)

from language.

munificent avatar munificent commented on May 29, 2024 9

Overloads are a useful, powerful feature. But they are also an enormous can of worms that add a ton of complexity to the language in ways that can be confusing and painful for users. Some examples off the top of my head:

Tear-offs

Presumably this is fine:

class C {
  bool method(int i) => true;
}

main() {
  var tearOff = C().method;
  var x = tearOff(3);
}

Now say you add an overload:

class C {
  bool method(int i) => true;
  String method(String s) => s;
}

main() {
  var tearOff = C().method; // ?
  var x = tearOff(3); // ?
}

What is the type of that tearOff now? Is the call to it valid? If so, what is the type of x?

We could say that it's an error to tear off a method that has overloads. But one of the primary goals with overloading is that it lets you add new methods to a class without breaking existing code. If adding an overload causes untyped tear-offs to become an error, then adding any overload anywhere is a potentially breaking change.

We could say that it's always an error to tear-off a method in a context where there is no expected type. That would be a significant breaking change to the language. That also means that changing an API to loosen an expected type (for example changing a callback parameter from a specific function type to Function) is now a breaking change.

Overriding and parameter types

Consider:

class A {
  void foo(int i) {}

  void bar(String s) {}
}

class B {
  void foo(num i) {}

  void bar(bool b) {}
}

Is B.foo() an override? Presumably yes. Is B.bar()? Probably not?

So what if you later change B to:

class B {
  void foo(num i) {}

  void bar(Object o) {}
}

Now that presumably will become an override of A.bar(). So the parameter types of a method may affect whether or not it's overriding a base class method.

Optional parameters

Consider:

class C {
  void method(int x) {}
  void method([int x, int y]) {}
}

main() {
  C().method(1);
}

Is that a valid set of overloads? If so, which one is called when you pass a single argument?

Promotion

Consider:

class C {
  void method(num n) { print('num'); }
  void method(int i) { print('int'); }
}

test(num n) {
  if (n is int) C().method(n);
}

I'm guessing this prints int. Are there other interactions between overload selection and type promotion that are less intuitive? Do users need a way to opt out of having promotion affect overload resolution?

Inference

Consider:

class C {
  void method(List<int> ints) {}
  void method(List<Object> objects) {}
}

main() {
  C().method([]);
}

What does this do?

Generics

Consider:

class C<T> {
  method(num n) {}
  method(T t) {}
}

Is that a valid pair of overrides? What if T is instantiated with num? int? Object?

What about:

class C<T extends num> {
  method(String s) {}
  method(T t) {}
}

Are those now valid overloads?

What about:

class A<T extends num> {
  method(T t) {}  
}

class B<T> extends A<T> {
  method(num n) {}
}

Is B.method() considered an overload of A.method() or an override?

I'm sure it gets even weirder when generic functions come into play.

Covariant

Here's a fun one:

class A {
  void method(num n) {}
}

class B extends A {
  void method(covariant int i) {}
  void method(Object? o) {}
}

Is the second method() in B a distinct overload, or does it collide with the other B.method()?

The point is not that all of these issues are unsolvable, but it's that they must be solved in order to add overloading to the language and users will have to understand at least a fraction of them whenever they interact with the feature.

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024 8

Java and C# yes, but JS can't do that

JS can by being untyped.

It is very common for JS functions/classes to handle multiple prototypes under the same name.
For instance the official Array:

new Array(length);
new Array(item, item2, item3, ...)

It's also why typescript supports methods overloads

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
 
}

from language.

passsy avatar passsy commented on May 29, 2024 8

This would really help for deprecations when arguments change

@Deprecated('Change the index and element')
Iterable<E> whereIndexed(bool Function(E element, int index) predicate){ }

// new signature with index and element swapped
Iterable<E> whereIndexed(bool Function(int index, E element) predicate){ }

from language.

matanlurey avatar matanlurey commented on May 29, 2024 6

@eernstg:

and if just one static overload is allowed to exist then every dynamic invocation will need to potentially handle static overloads

Why? If we just don't allow dynamic invocation to invoke static overloads, nothing is needed.

In particular, the very notion of making the choice among several method implementations of a method based on the statically known type is a completely different mechanism than standard OO method dispatch, and there is no end to the number of students that I've seen over time who just couldn't keep those two apart

I just want what is already implemented in Java/Kotlin, C#, or other modern languages. Do they do something we aren't able to do, or is this just about preserving dynamic invocation? As I mentioned, the alternative is users write something like this:

class Foo {
  void bar() {}
  void barString(String s) {}
  void barNumber(num n) {}
}

Not only do we punish users (they have to name and remember 3 names), dynamic invocation cannot help you here (mirrors could of, but that is no longer relevant).

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024 5

I'll join Aaron on that matter.
Typescript is another example. Vscode does a pretty good job.
Also, it arguably depends on good practices too. 

My biggest issue with the methodFoo vs methodBar practice is, it's very hard to give a proper name to a variant. This is especially problematic with non-nullable types.
With non-nullable types, there is a much bigger need for method variants, with Iterable.firstWhere as an example

In the ideal world, we'd have:

List<int> list;
// no orElse, the result is nullable
int? example = list.firstWhere(...);

// a orElse is passed, the returns is non-nullable
int example2 = list.firstWhere(..., orElse: () => 0);

But we can't do that anymore. I have some cases where I need 3-4 of these variants

This is something I've described in #836

from language.

munificent avatar munificent commented on May 29, 2024 4

All in all, I think context type based tear-off selection is possible. It might not always be predictable, but that's because we don't have a real model for what overloading means yet.

It's definitely possible because C# supports both overloads and tear-offs. Like you suggest, it goes off the context type. When you tear-off a method, it must be a context that unambiguously selects which overload you mean. Given that C# also supports type inference and user-defined conversion methods, that process is notoriously hairy... but it works.

from language.

munificent avatar munificent commented on May 29, 2024 3

I also think overloading would be a fantastically useful feature, but it's complexity is not to be under-estimated. If the language folks seem to cower in fear every time it comes up, that's not without reason. This tweet sums it up pretty well:

C# language design is

10% exploring cool ideas

75% overload resolution

15% being sad at past decisions we made

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024 3

I think the ability to reduce breaking changes and improve readability is important.
Whether it is achieved by method overload or less restricting callback assignations doesn't matter.

The current behavior causes problems. For example, in Rxdart, we have combineLatest2, combineLatest3, ..., combineLatest7.
This hurts readability. combineLatest2 could be interpreted as taking the 2 latest values.
This hurts discoverability too, as the IDE will suggest 7 times the same function in auto-complete. Library maintainers also need to document the same function many times.

Also, one of the dart goals is to be easy to pick up for JS/java/c# developers. All of which have their own way to handle multiple function prototypes under the same name.

from language.

rapus95 avatar rapus95 commented on May 29, 2024 3

It's much easier to read a few meaningful function names that have to scan a (potentially long) list of arguments (and although IDE-specific, most IDEs I've used can show multiple function names in a code completion list at once, but require you to toggle between overload info..

That's a fair point.
I want to repeat though, that when you sacrifice meaningful names for (overloading) less fitting names (the always-first argument against operator overloading, Dart has it nevertheless) you'll always run into those frustrating situations you describe. I'm just one of those guys who started many years ago with languages that had several restrictions options like visibility modifiers etc and now ended up with a mind that prefers languages that place the responsibility for beautiful code on the programmers rather than the language design. (Because after all, if a programmer wants to create ugly code, he'll accomplish it anyway.)

Regarding the readability of overload distinctions that's the fault of autocompleters which don't have first class support for overloading. If you dive into languages that have certain overloading mechanics at their core concepts (until you end up at multiple dispatch, hence my view from Julia) then you get increasingly better support for distinguishing different overloads/methods that all are behind the same function name (including meta information like where that given overload was defined). In the example with lots of arguments (maybe that's a bad situation either way) you refer to, the auto completer could start filtering the available options based on the arguments you already provided and order the remaining results based on your locally available variables.

In general, if your language supports overloading/multiple dispatch, the type signature is an integral part of the function's signature. Thus, autocompleters not showing that information together with the function name are simply not a good fit for it.

Tl;Dr: without proper tooling overloading will have a strong tendency towards getting messy.

from language.

Jetz72 avatar Jetz72 commented on May 29, 2024 3

What would happen if we removed the unique name requirement and did nothing else?

For one, without specifying any rules to decide how to prioritize or disambiguate them, determining what gets called becomes unclear:

void foo(int x, num y) => print("foo type 1!");

void foo(num x, int y) => print("foo type 2!");

void main() {
  foo(1, 2.0); //All is well, calls type 1.
  foo(3, 4); //Which one do we call here?
}

Tear-offs are a bigger issue:

var x = foo; //Which one? They both could fit into an int-int function type but what exact type is x?
if(x is void Function(int, num)) print("It's type 1!");
else if(x is void Function(num, int)) print("It's type 2!");
else if(x is void Function(int, int))
  print("It's not a reference to the function itself, it's some placeholder that kicks the discrepancy down the road "
  "to the invocation site and has an inexact type until then!");
else print("It's still a placeholder, but either we aren't computing the bound of foo's overloads or someone added "
  "another overload for foo which subtly changed the behavior here simply through its existence!");

from language.

lrhn avatar lrhn commented on May 29, 2024 3

Catch clauses are not methods. Yes, it would be nice to have a way to pass either onError: (e) { ... } or onError: (e, s) { ... } as async error handlers in a typed way, and overloading on parameter types could do that. So could union types. Or just always requiring (e, s) ...., which I'd do in a heartbeat if I could migrate the world easily.

For something (throwing) vs somethingOrNull, I'd probably have made more of them OrNull-versions originally if I knew then where Dart would end up, with null safety and null aware operators.
We didn't, and we can't add new methods to Iterable today, with or without overloading (but maybe with interface default methods).

Even then, the difference between something and somethingOrNull is not in the parameters, it's in the return type.
You can overload on that too, theoretically. I thought one of C# and Java actually did at some point (but maybe it's just the JVM which uses the return type as part of the signature, not the compiler function resolution). Most do not, and Dart likely also woudn't.

from language.

eernstg avatar eernstg commented on May 29, 2024 2

We can certainly still have dynamic invocations: If you use the type dynamic explicitly and invoke an expression of that type then you will get a dynamic invocation, and it is an important part of the Dart semantics that we have enough information available at run time to actually make that happen safely (that is, we will have a dynamic error if the invocation passes the wrong number of arguments, or one or more of the arguments has a wrong type, etc).

Apart from that, even with the most complete static typing you can come up with, it would still be ambiguous which method you want to tear off if you do x.foo and foo has several implementations. So it's more about first class usage (passing functions around rather than just calling them) than it is about static typing.

from language.

munificent avatar munificent commented on May 29, 2024 2

Either no tear-off support, or force users to write void Function(String) bar = foo.bar

I believe the latter is what C# does. It helps, though it causes some weird confusing behavior. It's always strange for users when you can't take a subexpression and hoist it out to a local variable.

Don't support overloading on bottom or top types

I don't think that's the cause of much of the pain.

Don't support overloading on generic types

That might help, but it's probably too painful of a limitation in practice. One of the key uses of overloading is being able to extend the core libraries without breaking them, and many of the classes where that would be most helpful, like Iterable and Future, are generic.

abstract class DiceRoller {
  /// Roll a dice defined by the expression "NdN".
  List<int> roll(String expression);

  /// Roll [amount] of dice with [sides].
  List<int> roll(int amount, int sides);

  /// ...
  List<int> roll(DiceRoll roll);
}

Yeah, I've run into this exact scenario.

Just allowing overloading by arity (number of parameters) would help many of these simple cases and doesn't require a lot of static typing shenanigans. Dynamically-typed Erlang supports it. Though it would interact in complex ways with optional parameters in Dart.

You don't have to worry about challenges around tear-offs.

Does that mean tear-offs wouldn't be supported for extension methods, or that it's easier?

That it's easier. Once you've don't the extension method lookup statically, you know exactly what method is being torn off, so you can just do it.

With overloading, there are interesting questions around whether the lookup should be done statically, dynamically, or some combination of both.

Things might get strange if we support generic extension classes

Do you mean (psuedo-syntax):

/// ['hello, 'world'].joinCustom()
extension String joinCustom(this Iterable<String> parts) {
  // ...
}

I mean:

extension class Iterable<int> {
   int sum() => fold(0, (sum, element) => sum + element);
}

test<T>(Iterable<T> elements) {
  elements.sum(); // <--???
}

I'm sure @leafpetersen and @lrhn can figure out how to handle all of this, but I'm not sure if I could. :)

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024 2

I disagree. We are just making the language silently accept more things that might not be intended.

How so? I don't quite see how can this lead to problems. We are talking about callbacks, not any function call.
Typescript has been doing it for years and there's no tslint rules preventing it. I haven't seen anybody complaining about it either. It's actually a big helps for a functional programming approach.

I find this healthier than having thousands of closures with _ as parameters everywhere.
Example:

PageRouteBuilder(
  pageBuilder: (_, __, ___) => Foo()
)

from language.

lrhn avatar lrhn commented on May 29, 2024 2

Even if we don't allow calling with more arguments than the static type allows[1], it would still allow a unary function to be assigned to a binary function type, which may or may not be a mistake. If we silently allow it, then it might hide some error.
That's always a trade-off: The more things you allow, the more permissive the language is, the fewer errors you can detect because the language assigns a meaning to the program anyway. On the other hand, the more restrictive you are, the more you require the programmer to jump through hoops to tell the language why something is OK. Different people have different preferences for this trade-off, and, as Matan shows, Google code tend to be on the very restrictive side, preferring to reject potentially wrong programs at the cost of some false positives and extra developer work, where Dart has traditionally been very permissive in allowing the programmer to decide what is correct rather than the language.

You can fit a restrictive language for a particular use-case, so you don't see overhead when sticking to that particular style, but it always comes up anyway.

Dart 2 is more restrictive than Dart 1 - for example a List<dynamic> is no longer a List<int>, and a class not implementing its interface is an error rather than a warning. Dart 2 is still more permissive than some other language, allowing you to do dynamic invocations at run-time. Some want that gone too, others do not.

What I do believe strongly is that Dart will need a way to evolve an API without immediately breaking all clients of the API. We currently do not have that, so any change to a public API is a breaking change. Packages might get away with breaking changes because of versioning (at the risk of "version hell"), but the platform libraries cannot.

Allowing "over applying" arguments to a function is a change that tips the trade-off more towards permissibility. It's perfectly safe from a language and type perspective, but it might hide some domain-semantic errors, where you are passing the wrong function and it's allowed anyway.

An explicit coercion might be a way to get the ability without the permissibility.
If I write stream.catchError(print!) and that automatically coerces print to (Object v1, StackTrace v2) => print(v1), then I get to cheaply (rather than freely) pass a lower-arity function where a higher-arity function is allowed, but I have to opt in to it at the actual coercion point.
It won't work for a declaration-site which wants to be permissible. We could have that too, so catchError was typed as

Future<T> catchError(FutureOr<T> Function(Object, StackTrace)! handler, [...])

which means that it auto-coerces its first argument if necessary.
That's just a local "annotation" on a static function type, not an intrinsic part of the function type itself. you can cast in and out of the auto-coercion for the same function.

So, yes, a tantalizing idea. Maybe not practically applicable in its raw form (you won't know without considering it and trying to see if the issues it introduces can be managed), but it has some positive perspectives that are worth remembering.

from language.

munificent avatar munificent commented on May 29, 2024 2

A dynamic call uses only the name to select the method. I guess it could also use the actual argument types to select the more precise one, but at the expense of having to do this dispatch at run-time. It's not clear which method to extract on a tear-off (we might be able to wing that based on context type, and fail if it's not enough, e.g., on a dynamic tear-off).

You can look at Dart's current optional parameters as defining "overloads" — there are multiple valid signatures you can use to invoke the function. There is "runtime dispatch" going on in that the function body must substitute in default values for parameters that are not passed.

In Dart today, when you tear-off a method that takes optional parameters you get all of the "overloads" bundled together — the function type retains optionality.

If we were to add real overloads, I think it would be great to preserve all of those properties. When you tear off a method, you get a callable object that retains all of the overloads for the given name. This means function types get pretty complex (!) — something like a intersection type for all of the signatures.

But it preserves dynamic calls, aligns with the existing behavior and is, I think, a very powerful, useful feature. Effectively multimethods, which would really set us apart from other statically-typed languages that can only do single dispatch.

What I do believe strongly is that Dart will need a way to evolve an API without immediately breaking all clients of the API. We currently do not have that, so any change to a public API is a breaking change.

+100

Packages might get away with breaking changes because of versioning (at the risk of "version hell"), but the platform libraries cannot.

I see this as an argument for making the platform libraries smaller and relying on external packages for more functionality. Doing that in a usable way probably means something like extension methods so that you don't sacrifice the nice object.method(...) syntax.

Of course, combining extension methods and runtime-dispatched overloads isn't easy. :)

from language.

DanTup avatar DanTup commented on May 29, 2024 2

I've seen the issue described above in TypeScript and it can be pretty annoying. Something changes in a library/API so that my call is wrong. Navigating through the overloads to try and figure out what I need to change can be hard as the compiler often picks the wrong overload for the message:

Screenshot 2020-05-06 at 12 24 53

Here, the error here is telling me this argument must be WorkspaceFolder | TaskScope but actually it's correct as a string - the compiler is assuming the wrong overload. It's made worse by VS Code's bad rendering of the arguments in the signature help (it's a blob of white with no coloring), particularly in TypeScript where the signatures can include large object type literals.

So IMO, the difference between overloads and fromX/fromY/etc. is that you get to select the overload yourself and force the compiler to give the correct error message for the one you want, rather than relying on it guessing and potentially giving a spurious message.

However, there are also some cases where I think overloads work better. For example I have this function (if I'm interacting with node's FS APIs, I'll have string paths, but when interacting with VS Code APIs, I'll have file URIs):

export async function getLaunchConfiguration(script: vs.Uri | string) {
	if (script instanceof vs.Uri)
		script = fsPath(script);
        // script is now always a string path

In lieu of unions, this would work as an overload. Having two separate getLaunchConfigForUri and getLaunchConfigForPath functions feels unnecessary.

from language.

rapus95 avatar rapus95 commented on May 29, 2024 2

I'm also against unnecessary bulk overloading, though, I don't see where you would overload if it's unnecessary. If there are good reasons for overloading several times (like mathematical functions) then just go ahead and do it, if multiple overloads don't share an abstract meaning, then place them in different scopes so that they don't collide/interfere.

IMO, only overload if the variant you want to add is the canonical one which a user would expect behind those arguments for the given function name. If that's not the case, use a function name which makes it clear what to expect when feeding it with the given arguments.

from language.

Ryacinse avatar Ryacinse commented on May 29, 2024 2

But Kotlin has both optional parameters and overloading. And optional parameters in Kotlin is more flexible.

from language.

eernstg avatar eernstg commented on May 29, 2024 2

And optional parameters in Kotlin is more flexible

The situation is somewhat mixed.

For instance, Kotlin function types are typed statically as FunctionN<R, T1 .. TN> (for a function accepting N parameters), and there is no subtype relationship between functions with a different number of formal parameters. This means that Kotlin can't safely abstract over functions which are callable using the same argument list shape (e.g., functions that accept one positional parameter of some type T), but where the run-time type is actually a function whose type accepts some optional parameters. In general, a Dart function type can be a subtype of another Dart function type because it accepts a larger set of optional parameters, but Kotlin can't support that.

This also prevents overriding method declarations in Kotlin from adding new optional parameters (you'll get the error that it doesn't override anything).

So there are several ways in which Dart is more flexible than Kotlin. Kotlin can support a completely untyped version of this kind of abstraction using reflection (so you can obtain the arity of a Function and then call it with that many parameters, and you won't get any default values), but Dart does it smoothly and in a way which is statically type safe.

from language.

Levi-Lesches avatar Levi-Lesches commented on May 29, 2024 2

@lukepighetti, types are already considered part of the signature. It's how Dart lets you know when you make an invalid override. But throwing overloading into the mix can make things complicated. Consider the following classes:

/// A pair of 2D coordinates that can be decimals or integers. 
class Coordinates {
  final num x, y;
  const Coordinates(this.x, this.y);
  
  // Adds two coordinates together.
  operator +(covariant Coordinates other) => 
    Coordinates(x + other.x, y + other.y);
}

/// A pair of 2D coordinates that can only be integers. 
class IntegerCoordinates extends Coordinates {
  @override
  final int x, y;
  const IntegerCoordinates(this.x, this.y) : super(x, y);
  
  /// Only adds IntegerCoordinates together. 
  ///
  /// This ensures the resulting coordinates are also integers. 
  @override
  operator +(IntegerCoordinates other) => 
    IntegerCoordinates(x + other.x, y + other.y);
}

This is a common pattern where you subclass a type to make it more restricted. In this case, IntegerCoordinates can only contain integers and can only be added to other integers to keep the condition met. In today's Dart, that gives you this:

void main() {
  final a = Coordinates(0.5, 0.5), b = Coordinates(1.5, 1.5);
  final c = IntegerCoordinates(1, 1), d = IntegerCoordinates(2, 2);
  
  // [Coordinates] can be added to any other subtype of [Coordinates].
  print(a + b);  // Coordinates(2.0, 2.0)
  print(a + c);  // Coordinates(1.5, 1.5)
  
  // [IntegerCoordinates] can only be added to themselves. 
  print(c + d);  // IntegerCoordinates(3, 3)
  print(c + a);  // Error: Coordinates cannot be assigned to IntegerCoordinates
}

With overloading, does this still hold? Or is IntegerCoordinates.+ no longer an override of Coordinates.+ but rather a whole new method, so that c + a is now valid?


Dart isn't Java, but since Java has overloading and overriding, I thought I'd bring a Java example to compare. Due to Java using a different type of number system than Dart, I repurposed the example into one that makes less logical sense but still shows the problem:

class Car {
  void crash(Car other) { System.err.println("Two cars crashed!"); }
} 

class Boat extends Car {
  // Turns out this is an *overload*, not an override, of Car.crash. 
  void crash(Boat other) { System.err.println("Two boats crashed!"); }
}

public class Temp {
  public static void main(String[] args) {
    final Car car1 = new Car(), car2 = new Car();
    final Boat boat1 = new Boat(), boat2 = new Boat();

    car1.crash(car2);  // Car.crash
    car1.crash(boat2);  // Car.crash

    boat1.crash(boat2);  // Boat.crash
    boat1.crash(car1);  // This shouldn't work, but it calls Car.crash 
  }
}

from language.

jodinathan avatar jodinathan commented on May 29, 2024 1

but with a sound dart we don't have dynamic invocations, do we?

from language.

nex3 avatar nex3 commented on May 29, 2024 1

Users aren't going to know to run a tool to tell them that overloads are breaking changes any more than they're going to know that overloads are breaking changes. And even if they did, the fact that adding an overload requires incrementing a package's major version would make the feature much less useful for anyone with downstream users.

I don't think a lint would do anything, because upstream API authors don't control whether their downstream users dynamically invoke their APIs. In fact, since we don't have robust and universal support for --no-implicit-dynamic, the downstream users probably also don't know when they're dynamically inovking APIs.

from language.

yjbanov avatar yjbanov commented on May 29, 2024 1

This tweet sums it up pretty well

It's nice of the author to leave a hint though: "I would do it more like F#. It is there in a very basic, simple form" 😄

from language.

munificent avatar munificent commented on May 29, 2024 1

Do we have a requirement that all new features support dynamic invocation?

I think dynamic invocation is red herring. C#'s dynamic supports full C# overload resolution. The complexity isn't around dynamic invocation. It is more inherent to how overload resolution interacts with type inference, overriding, generics, generic methods, optional parameters, and implicit conversions.

(We don't have implicit conversions in Dart yet, but we will once you can pass 0 to a method that expects a double.)

I just slapped this together, but here's a sketch that might give you a flavor of how it can get weird:

class Base {
  bar(int i) {
    print("Base.bar");
  }
}

class Foo<T extends num> extends Base {
  bar(T arg) {
    print("Foo<$T>.bar");
  }
}

test<T extends num>() {
  Foo<T>(null);
}

main() {
  test<int>();
  test<double>();
}

from language.

munificent avatar munificent commented on May 29, 2024 1

Potentially, yes, but I think they tend to be simpler. With extension methods, you still only have a single "parameter" you need to dispatch on. You don't have to worry about challenges around tear-offs. Things might get strange if we support generic extension classes. I don't know. But I would be surprised if extension methods weren't easier than overloading.

from language.

lrhn avatar lrhn commented on May 29, 2024 1

Overloading and extension methods are orthogonal. Both allow "adding a method" to a class without breaking an existing method with the same name. If you have both, there is a good chance that the extension method won't completely shadow the original method. Extension methods are not virtual, which is annoying. You can add them from the side, which is useful. We don't have a way to add a virtual method from the side, and I'm not sure it's possible.

The languages with overloading mentioned so far do not have optional parameters the same way Dart does. They do have optional positional parameters, so that might not be an issue.
We still have to handle cases like:

  int foo(int x, [int y, int z]) => ...
  int foo(int x, {int y, int z}) => ...
  ...
     theFoo.foo(42);

Likely it's just an unresolved overload error at compile-time. Again, a dynamic invocation might not apply here, but if it does, then it's not clear that there is a solution.
Maybe we can solve it by (theFoo.foo as int Function(int, int, int))(42). I'd like as to actually induce a preference on the expression.

As for

extension class Iterable<int> {
   int sum() => fold(0, (sum, element) => sum + element);
}
test<T>(Iterable<T> elements) {
  elements.sum(); // <--???
}

my way of figuring that one out would just be to say "extension method does not apply". The static type of elements is Iterable<T>, which is not the same as, or a subtype of, Iterable<int>, so the static extension method cannot be used. Since elements does not have a sum method, your program won't compile.
Now, if it had been:

test<T extends int>(Iterable<T> elements) {
  elements.sum(); // <--???
}

then the extension method would likely have applied.

More controversial is:

extension List<T> {
  R join<R>(R base, R Function(R, T) combine) => ...;
}

Should that function "override" the join function on List? Shadow join completely, or only act as an alternative overload? What if I named it fold instead? That kind of conflict is troublesome, but probably not a real issue (it'll just allow people to shoot themselves in the foot, and linting can tell you to stop being silly).

Anyway, this is about overloading, not extension methods.

from language.

nex3 avatar nex3 commented on May 29, 2024 1

Not allowing optional named parameters with overloads will make it difficult to backwards-compatibly extend APIs that were originally defined as overloads. This would incentivize API designers to add lots of overloads with additional positional parameters, which is generally less readable and more fragile than named parameters.

My preference for APIs like

  int foo(int x, [int y, int z]) => ...
  int foo(int x, {int y, int z}) => ...

would be to disallow the definition at compile time. This ensures that overload calls are never ambiguous, and that API designers are aware when they try to design an API that would be ambiguous and can avoid it (e.g. by making int y mandatory in the first definition above).

from language.

lrhn avatar lrhn commented on May 29, 2024 1

Making void Function(Object) assignable to void Function(Object, StackTrace) is a tantalizing idea.

It would mean that you can call any function with more arguments than it expects, the extra arguments are just "lost". It's really the dual of optional parameters.
You can also see it like every function value actually has all optional parameters (infinite positional parameters, all names) that it just doesn't use, and declaring parameters makes the type more restrictive in that the argument doesn't accept everything any more, and it might be required.

It would make it harder to detect some errors, but we can still make it an error to pass more arguments than the static type expects (there is no idea what that argument means), so it's only invalid dynamic calls, and you're really asking for it there.

It would also allow a super-class to add more arguments. Subclasses which don't expect these arguments are still valid. That might actually be a problem, because there will be no hint that the subclass didn't omit the parameter deliberately, and it's likely to not be able to act correctly on calls that pass that parameter and expects it to have a meaning.
Still, adding parameters to superclasses is huge, since it's currently a completely breaking change.

As for overloading, that makes it much harder for Dart to maintain its dynamic behavior. A dynamic call uses only the name to select the method. I guess it could also use the actual argument types to select the more precise one, but at the expense of having to do this dispatch at run-time. It's not clear which method to extract on a tear-off (we might be able to wing that based on context type, and fail if it's not enough, e.g., on a dynamic tear-off). Overriding not a clear match for the Dart object model, it will need to be designed in a way that is both backwards compatible and forwards usable. I have no idea whether that is actually simple or not :)

from language.

matanlurey avatar matanlurey commented on May 29, 2024 1

@lrhn:

is a tantalizing idea.

I disagree. We are just making the language silently accept more things that might not be intended. There would be a lint created for this and enforced in Flutter and Google Day 1 this feature is added to the language.

As for overloading, that makes it much harder for Dart to maintain its dynamic behavior

We don't want dynamic behavior (https://github.com/dart-lang/sdk/issues/26488#issuecomment-402532000). The alternative to adding overloads in the language is to have users write the overloads themselves (https://github.com/dart-lang/sdk/issues/26488#issuecomment-401424881) - either with specialized methods or named constructors. Neither of those support dynamic invocation either, so that argument doesn't hold weight for me.

from language.

lrhn avatar lrhn commented on May 29, 2024 1

@munificent

We can definitely treat optionally-parameterized functions as implementing multiple overloads in one declaration. Tearing off all overloads in one go is a big mouthful, but we could aim lower and allow you to tear off a sub-set of the methods which can be described by a single function type (including optional parameters). The tear-offs might not cover the entire function type, and hitting the holes will cause errors, but it's still typable without having to introduce general union types.

I see this as an argument for making the platform libraries smaller and relying on external packages for more functionality.

If we can make the platform libraries smaller, then we have already solved the problem of making at least some breaking changes to the platform libraries. :)

from language.

munificent avatar munificent commented on May 29, 2024 1

Serious question: do we know real examples of APIs which could benefit from method overloading?

Sure, look at any Java or C# API. Overloading is used heavily and often in patterns that don't map to Dart's support for trailing optional parameters.

A much closer example is +. int + int should return int, while int + double returns double. Right now, the language specification special-cases that operator and a couple of others. Basically there are a handful of overloaded methods. Sort of a "overloading for me but not for thee" situation.

But, since each of those has to be special-cased in the spec, some aren't covered. It would be nice if int.clamp(int, int) returned an int, but it doesn't. It returns num.

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024 1

A more obvious example that is currently impossible with dart optional parameters: Different return types

Other languages allow defining a compose or pipe function, which fuses multiples functions into one. Very useful for functional programming.

The problem is that is has a prototype similar to the following:

R pipe<T, T2, R>(Func1<T, T2> cb, Func1<T2, R> cb2);
R pipe<T, T2, T3, R>(Func1<T, T2> cb, Func1<T2, R> cb3, Func1<T3, R> cb3);

In dart we'd have to have different name for the same function. Which leads to a pipe2, pipe3, ...

Rxdart library faces this exact problem with combineLatest operator. Where it defined 7 times the same function with different names

from language.

munificent avatar munificent commented on May 29, 2024 1

I want to provide another reference to a language where multiple dispatch (in combination with duck typing) is the thing that enables high performance (& increases code reuse), Julia.

This is true, and really fantastic, but it does rely on always having a JIT available, allocating lots of memory for code, and having relatively slow-startup and warm-up times. Those all make sense for a language like Julia, but wouldn't be a good fit for Dart where we need to run fast ahead-of-time compiled applications on relatively underpowered end user mobile devices.

This is not to rule out multiple dispatch entirely, just that Julia's "throw everything at the JIT and let it specialize the world" approach won't get Dart there.

from language.

rapus95 avatar rapus95 commented on May 29, 2024 1

@Hixie I come from Julia where we do have lots of overloads (though, currently miss some convenience tooling which prematurely exposes all missing functions that need to be declared first). Thus, IMO what you describe depends on a strong type inference while typing. Because if the IDE tools know the types of your expressions, they can infer which of those multiple definitions can be called (this enhances the suggestions) and once you settled on an existing combination, it can also infer the return type. Thus, I'm not sure, whether that's an objection that holds against the design. It rather requires good tooling. Which Dart already has with first level support. Thus, if it ever will be added, tooling will be covered I guess.

from language.

jodinathan avatar jodinathan commented on May 29, 2024 1

When I was developing in C# few months ago, I didn't like when a method had dozens of overloads. This happen with TypeScript as well because of the JS burden it carries.
However, it was very nice when it was 2, maybe 3 overloads.

If I would use method overload, and I really would like to be able to, I would also like to have a lint that prevents overloading a method more than 3 times. Or something like that.

Method overloading is not to spam methods because maybe someone would like to use it with different arguments, but to cut out the need to create some obvious variants of the method.
The null/nonNull variants are examples that I am trying to take as of a good use of overloading.
Bad use would be something like parsing a date. Parsing should used with a string argument, if you need to get a date from an int, then maybe there should be a fromTimestamp method.

Obviously, that is my point of view =]

from language.

leafpetersen avatar leafpetersen commented on May 29, 2024 1

Is this at sight?

This is not currently on our short to medium term road map.

from language.

subzero911 avatar subzero911 commented on May 29, 2024 1

+1 for function overloading
You've added rather complicated language concepts like strong typing, covariants, enum classes, null-safety, and you even heading to add ADT.
But you didn't add such a basic OOP feature like function overloading because "it will make Dart difficult", that's just ridiculous!

from language.

lukepighetti avatar lukepighetti commented on May 29, 2024 1

My preference, and it may be oversimplifying things, is to add type and parameter names to method signature

class Foo {
  bool isMonday({required DateTime fromDateTime}) {}

  /// collides with previous method, because the signature does not extend into the params
  /// would be nice if this was treated as a separate method from the one above
  bool isMonday({required Date fromDate}) {}
}

It would be particularly useful in making operators that handled different units

extension on Distance {
  Distance operator * (num other) {
    return Distance(this.meters * other);
  }

  Area operator * (Distance other) {
    return Area(this.meters * other.meters);
  }
}

from language.

Levi-Lesches avatar Levi-Lesches commented on May 29, 2024 1

1. Dart already has overloading in the try catch clause. If overloading is not something necessary, don't you agree that all catch should be something like catch(Exception e, [StackTrace st])?

@munificent brought up valid questions and complications that arise with methods, so comparing it to try/catch isn't a valid response because it doesn't answer those questions.

2. Is there any plan to not have these many somethingOrNull like firstWhereOrNull without losing performance?

I agree with this but the ideal answer, IMO, would be to make firstWhere nullable in the first place to cover all cases, not overloading.

The point is simplicity. You don't want to see the StackTrace argument everywhere as you don't want to have firstWhereOrNull kind of method.

Simplicity is good, but the sheer amount of syntactic and semantic questions that arise when using overloads negates this. If you have to ask yourself which method you're using, I don't see how that's simpler than firstWhere/firstWhereOrNull. Sure, the latter is more verbose, but you never have to ask yourself what arguments/return values are valid because it's in the name. Doesn't get much simpler than that. Overloads, however, mean you always have to check the source code to see which types you can use because there may be more than one implementation with the same name.

For example, imagine parsing a DateTime. Right now there are several options: .fromMicrosecondsSinceEpoch, .fromMillisecondsSinceEpoch, .utc, .parse, and .tryParse. Sure these names are quite verbose, but imagine trying to figure out what value to pass if there were just one DateTime.parse constructor.

from language.

jodinathan avatar jodinathan commented on May 29, 2024

any news on this?
maybe with dart 2.0?
=]

from language.

lrhn avatar lrhn commented on May 29, 2024

Not in Dart 2.
This is a significant change to the object model of Dart.
Currently, a Dart object has at most one accessible member with any given name. Because of that, you can do a tear-off of a method.
If you could overload methods, tear-offs would no longer work. You would have to say which function you tore off, or create some combined function which accepts a number of different and incompatible parameter signatures.
It would make dynamic invocations harder to handle. Should they determine that method to call dynamically? That might cause a significant code overhead on ahead-of-time compiled programs.

I don't see this happening by itself. If we make a large-scale change to the object model for other reasons, then it might be possible to accommodate overloading too, but quite possibly at the cost of not allowing dynamic invocations.

from language.

eernstg avatar eernstg commented on May 29, 2024

@matanlurey,

Is it being dynamically invokable a requirement? I don't think it is.

That was actually the point I was making: It is important that there is a well-defined semantics of method invocation, and if just one static overload is allowed to exist then every dynamic invocation will need to potentially handle static overloads, and that presumably amounts to multiple dispatch (like CLOS, Dylan, MultiJava, Cecil, Diesel, etc.etc.), and I'm not convinced that it is a good trade-off (in terms of the complexity of the language and its implementations) to add that to Dart.

In particular, the very notion of making the choice among several method implementations of a method based on the statically known type is a completely different mechanism than standard OO method dispatch, and there is no end to the number of students that I've seen over time who just couldn't keep those two apart. (And even for very smart people who would never have a problem with that, it's likely to take up some brain cells during ordinary daily work on Dart projects, and I'm again not convinced that it's impossible to find better things for those brain cells to work on ;-).

from language.

nex3 avatar nex3 commented on May 29, 2024

It's worth mentioning that if we decide to support overloads without dynamic invocations, this means that adding an overload to an existing method will be a breaking change--one that probably won't be obvious to API designers.

from language.

matanlurey avatar matanlurey commented on May 29, 2024

Depending how we do it, we theoretically could support a dynamic fallback overload:

class Future<T> {
  // This one is used for any dynamic invocations only.
  Future<T> catchError(dynamic callback);
  Future<T> catchError(void Function(Object));
  Future<T> catchError(void Function(Object, StackTrace));
}

It's not clear to me this is particularly worth it, though. Other hotly requested features like extension methods would also suffer from being static only, and changing a method from invocation to extension would be a breaking change.

from language.

nex3 avatar nex3 commented on May 29, 2024

I expect it won't be too surprising to users that changing an existing method is a breaking change. Adding a new method being a breaking change, on the other hand, is likely to be very surprising, especially since it's safe in other languages that support overloading.

from language.

matanlurey avatar matanlurey commented on May 29, 2024

Right, because they never supported dynamic invocation (or only do, like TypeScript).

One project @srawlins was working on back in the day was a tool that could tell you if you accidentally (or on purpose) introduced breaking changes in a commit. I imagine a tool could help, or we could even add a lint "avoid_overloads" for packages that want to be dynamically-invoke-able.

from language.

matanlurey avatar matanlurey commented on May 29, 2024

OK, I think we can note that this feature would be breaking for dynamic invocation and leave it at that.

The language team hasn't given any indication this particular feature is on the short-list for any upcoming release, and I'm assuming when and if they start on it we can revisit the world of Dart (and what support we have for preventing dynamic invocation entirely).

I would like to hope this issue continues to be about implementing the feature, not whether or not it will be a breaking change (for all we know this will happen in Dart 38, and dynamic invocation has been disabled since Dart 9).

EDIT: For anyone reading this, I am not saying that will happen.

from language.

matanlurey avatar matanlurey commented on May 29, 2024

@munificent Definitely understand it shouldn't be underestimated. Do we have a requirement that all new features support dynamic invocation? If so, didn't we already break that with type inference?

from language.

eernstg avatar eernstg commented on May 29, 2024

I'm with @munificent on the need to recognize the complexity ('C#: 75% is overload resolution' ;-), but I'm not worried about the complexity of specifying or even implementing such a feature, I'm worried about the complexity that every Dart developer is involuntarily subjected to when reading and writing code. In particular, I'm worried about the inhomogeneous semantics where some decisions are based on the properties of entities at run time, and other decisions are based on properties of entities produced during static analysis—one is fine, the other is fine, but both at the same time is costly in terms of lost opportunities for developers to think about more useful things.

One way we could make the two meet would be based on a dynamic mechanism that compilers are allowed to compile down to a static choice whenever that's guaranteed to be correct. For instance, using the example from @matanlurey as a starting point:

abstract class Future<T> {
  ...
  Future<T> catchError(Function)
  case (void Function(Object) onError)
  case (void Function(Object, StackTrace) onError)
  default (Function onError);
  ...
  // Could be a bit nicer with union types.
  Future<T> catchError2(void Function(Object) | void Function(Object, StackTrace))
  case (void Function(Object) onError)
  case (void Function(Object, StackTrace) onError);
}

class FutureImpl<T> implements Future<T> {
  Future<T> catchError
  case (void Function(Object) onError) {
    // Implementation for function accepting just one argument.
  }
  case (void Function(Object, StackTrace) onError) {
    // Implementation for function accepting two arguments.
  }
  default (Function onError) => throw "Something";
  ...
  // The variant with union types would just omit the default case.
}

There would be a single method catchError (such that the tear-off operation is well-defined and preserves the full semantics), and the semantics of the declared cases is simply like a chain of if-statements:

  Future<T> catchError(Function onError) {
    if (onError is void Function(Object)) {
      // Implementation for function accepting just one argument.
    } else if (onError void Function(Object, StackTrace) onError) {
      // Implementation for function accepting two arguments.
    } else {
      default (Function onError) => throw "Something";
    }
  }

However, the declared cases are also part of the interface in the sense that implementations of catchError must handle at least the exact same cases, such that it is possible to generate code at call sites where it is statically known that the argument list satisfies a specific case. In that situation we would have code that directly calls one of those cases (such that there is no run-time penalty corresponding to the execution of a chain of if-statements, we have already chosen the correct branch at compile-time).

For instance, we always know everything about the type of a function literal at the call site. Special types like int and String are constrained by the language such that we can't have one instance which is both at the same time, and with sealed classes we can have more cases with that property.

This means that we will have multiple dispatch in a way where the priority is explicitly chosen by the ordering of the cases (so we avoid the infinite source of complexity which is "ambiguous message send"), and the mechanism will double as a static overloading mechanism in the cases where we have enough information statically to make the choice.

I'm not saying that this would be ridiculously simple, but I am saying that I'd prefer working hard on an approach where we avoid the static/dynamic split. And by that split I don't mean code which is full of expressions of type dynamic, I mean code which may be strictly typed (--no-implicit-cast and whatnot), because that would still allow the same operations applied to the very same objects to behave differently, just because the type checker doesn't know the same amount of things at two different call sites.

... Java/Kotlin, C#, or other modern languages. Do they do something we
aren't able to do, or is this just about preserving dynamic invocation?

Neither (we can surely make a big mess of things as well ;-), but, to me, it is very much about avoiding a massive amount of subtleties for the developers, also for code which is statically typed to any level of strictness that we can express.

from language.

matanlurey avatar matanlurey commented on May 29, 2024

Most Dart developers don't want dynamic invocation (in Dart2, it is actively bad in many places with reified types), so it seems to me trying to preserve that feature for new language features isn't worth the time or effort.

from language.

eernstg avatar eernstg commented on May 29, 2024

@matanlurey, if that's concerned with this comment, it sounds like maybe you did not notice that I'm not talking about dynamic invocations, or certainly not only about them:

I don't mean code which is full of expressions of type dynamic, I mean .. strictly typed ..
[code that still causes a] massive amount of subtleties for the developers

from language.

matanlurey avatar matanlurey commented on May 29, 2024

I might be ignorant, but isn't there a similar set of complexity for extension methods? Meaning that if we need to eventually figure out how to dispatch extension methods, at least some of the same logic holds for dispatching overload methods?

(It looks like, casually, most OO languages that support extensions support overloading)

from language.

matanlurey avatar matanlurey commented on May 29, 2024

Thanks for this! A few more questions, but don't feel like they are important to answer immediately :)

Potentially, yes, but I think they tend to be simpler.

Are there some limitations we could add to overloads to make them easier to implement and grok? I might be incredibly naive ( /cc @srawlins ) but I imagine 95%+ of the benefit could be gained with a few simplifications:

  • Either no tear-off support, or force users to write void Function(String) bar = foo.bar
  • Don't support overloading on bottom or top types
  • Don't support overloading on generic types

For example, today I was writing a sample program for a r/dailyprogramming question.

I wanted to be able to write:

abstract class DiceRoll {
  int get amount;
  int get sides;
}

abstract class DiceRoller {
  /// Roll a dice defined by the expression "NdN".
  List<int> roll(String expression);

  /// Roll [amount] of dice with [sides].
  List<int> roll(int amount, int sides);

  /// ...
  List<int> roll(DiceRoll roll);
}

But I'd either have to write:

abstract class DiceRoller {
  /// Roll a dice defined by the expression "NdN".
  List<int> rollParse(String expression);

  /// Roll [amount] of dice with [sides].
  List<int> roll(int amount, int sides);

  /// ...
  List<int> rollFor(DiceRoll roll);
}

Or do something extra silly like:

abstract class DiceRoller {
  List<int> roll(dynamic expressionOrAmountOrRoll, [int sides]) {
    if (expressionOrAmountOrRoll is int) {
      if (sides == null) {
        throw 'Expected "sides"';
      }
      return _rollActual(expressionOrAmountOrRoll, sides);
    }
    if (sides != null) {
      throw 'Invalid combination';l
    }
    if (expressionOrAmountOrRoll is String) {
      return _rollAndParse(expressionOrAmountOrRoll);
    }
    if (expressionOrAmountOrRoll is DiceRoll) {
      return _rollActual(expressionOrAmountOrRoll.amount, expressionOrAmountOrRoll.sides);
    }
    throw 'Invalid type: $expressionOrAmountOrRoll';
  }
}

The former is hard for the users to use (and find APIs for) and the latter sucks to write, test, and basically forgoes any sort of static type checking.

You don't have to worry about challenges around tear-offs.

Does that mean tear-offs wouldn't be supported for extension methods, or that it's easier?

I imagine folks would find it weird if you could do:

void main() {
  // This will, or will not work, depending on if `map` is an extension method or not?
  wantsAClosure(['hello', 'world'].map);
}

void wantsAClosure(Iterable<String> Function(String) callback) {}

Things might get strange if we support generic extension classes

Do you mean (psuedo-syntax):

/// ['hello, 'world'].joinCustom()
extension String joinCustom(this Iterable<String> parts) {
  // ...
}

Or:

extension Map<K, V> groupBy<K, V>(this Iterable<V>, K Function(V) groupBy) {
  // ...
}

from language.

matanlurey avatar matanlurey commented on May 29, 2024

Thanks! I am sure I will understand this issue eventually :)

One of the key uses of overloading is being able to extend the core libraries without breaking them, and many of the classes where that would be most helpful, like Iterable and Future, are generic.

Did you mean one of the key uses of extension methods, or overloading?

from language.

jodinathan avatar jodinathan commented on May 29, 2024

from language.

matanlurey avatar matanlurey commented on May 29, 2024

I also tend to agree that trying to combine overloads and optional parameters (either named or positional) is probably not worth its weight. A lot of the places that optional parameters are used today are to emulate overloads, and users would likely use overloads instead if available.

from language.

matanlurey avatar matanlurey commented on May 29, 2024

Not allowing optional named parameters with overloads will make it difficult to backwards-compatibly extend APIs that were originally defined as overloads. This would incentivize API designers to add lots of overloads with additional positional parameters, which is generally less readable and more fragile than named parameters.

I mean, if that allows overloads 5 years sooner than trying to capture all use cases, I'd still prefer it - the reality looks like even if we got the language team to agree in principal, needing to capture all uses cases (dynamic invocation, optional parameters, etc) would likely be such an effort that it would not be prioritized.

It would be trivial to move APIs that have heavy use of optional positional parameters to use overloads. Here is @munificent's example case:

// Before
int range(int minOrMax, [int max]);

// After
int range(int max);
int range(int min, int max);

For the named case, its not easier, but the option here is to just not change APIs that used named parameters to use overloads.

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024

Somehow I see a lot in this conversation the need of overload to add a new callback parameter like in :

  Future<T> catchError(void Function(Object));
  Future<T> catchError(void Function(Object, StackTrace));

I think another solution to this problem would be to make void Function(Object) assignable to void Function(Object, StackTrace)

Typescripts allows it. It's pretty useful for many methods such as Array.map((T, index) => S) where we usually don't need index.

It won't solve all overloads problems, but will definitely help.

from language.

RdeWilde avatar RdeWilde commented on May 29, 2024

Would really like this. Most major languages support it.

from language.

natebosch avatar natebosch commented on May 29, 2024

It would mean that you can call any function with more arguments than it expects, the extra arguments are just "lost". It's really the dual of optional parameters.

For cross-linking purposes this was also discussed in dart-lang/sdk#10811

from language.

matanlurey avatar matanlurey commented on May 29, 2024

Dart has different calling conventions than JavaScript, so it's not a 1:1 fit.

from language.

RdeWilde avatar RdeWilde commented on May 29, 2024

Also custom operators can act differently based on the parameter type. For example all sorts of numerics (float, int, bigint, etc).

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024

If dart ever has non-nullable types (and looks like it will), then this feature has an entirely new usage.

There are some situations where with custom parameters, the prototype may switch between nullable and non-nullable.
Something that would not be possible with our current optional parameters.

from language.

munificent avatar munificent commented on May 29, 2024

This is a good point. Do you have any concrete APIs you can point us at where this comes into play?

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024

StreamBuilder is a potential use-case due to its initialData.

In a non-nullable world, the StreamBuilder API could evolve to make it null safe:

StreamBuilder<T>({
  @required Stream<T> stream,
  T initialData,
  @required Widget builder(BuildContext context, T value, ConnectionState state),
  Widget errorBuilder(BuildContext context, dynamic error, ConnectionState state), 
});

We have to scenarios here:

  • initialData is omitted => T value of builder is nullable
  • initialData is present => T value is not nullable (unless T itself is nullable)

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024

It basically fits with any optional parameters that depend on other parameters.

Consider the following function prototype:

foo({ bar, baz });

Sometimes we may do things like:

assert(bar != null || baz != null)

which could translate into:

foo({ @required bar, baz });
foo({  bar, @required baz });

or

assert((bar != null && baz == null) || (baz != null && bar == null));

which translates into:

foo({ @required bar });
foo({ @required baz });

A more concrete example is thePositionned widget where we cannot specify simultaneously all arguments.

Another example is DecoratedBox, where we cannot specify both color and decoration together.

from language.

nex3 avatar nex3 commented on May 29, 2024

Iterable.firstWhere() is a core library example. Ideally it would have three signatures:

E firstWhere(bool test(E element));
E firstWhere(bool test(E element), {E orElse()});
E? firstWhere(bool test(E element), {E? orElse()});

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024

I thought about that example too. But in that situation, the operator ?? comes in handy and orElse is basically removed.

from language.

nex3 avatar nex3 commented on May 29, 2024

firstWhere() throws an error if orElse isn't passed and no element matches, so you'd need to write firstWhere(..., orElse: () => null) ?? ..., which is definitely worse than firstWhere(..., orElse: () => ...). And even still, you'd want the call without orElse to return a non-nullable type.

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024

Indeed, but that exception doesn't make sense anymore in a world with non-nullable types.

The prototype could be fixed to:

E? firstWhere(cb)

Which leads to:

Iterable.firstWhere(cb) ??  orElse

from language.

nex3 avatar nex3 commented on May 29, 2024

That API wouldn't work very well for iterables that may include null values that don't match the callback.

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024

I would say that in this situation the real gain with methods overload is to pass the prototype change as non-breaking but deprecated.

So that we basically have

E? firstWhere(bool cb(E value));
@deprecated
E firstWhere(bool cb(E value), { E orElse() });

That API wouldn't work very well for iterables that may include null values that don't match the callback.

I don't really understand your message, sorry. But other languages offer a very similar firstWhere so I don't see any issue.
This may be a bit off-topic though.

from language.

ORESoftware avatar ORESoftware commented on May 29, 2024

@rrousselGit

Also, one of the dart goals is to be easy to pick up for JS/java/c# developers. All of which have their own way to handle multiple function prototypes under the same name.

Java and C# yes, but JS can't do that - only one field/method with a given name. Since one of the main purposes of Dart is to transpile to JS, I think this is what makes implementing overloading an afterthought in Dart?

from language.

rapus95 avatar rapus95 commented on May 29, 2024

Having read only about half of the comments and searching for references to Julia (and not found them), I want to provide another reference to a language where multiple dispatch (in combination with duck typing) is the thing that enables high performance (& increases code reuse), Julia. In combination with Constant Propagation and Cross-Function-Optimization this can be very powerful! (See also GraalVM/Truffle which aim for similar optimization steps, without multiple dispatch though)
If you want a clear to follow talk about why this works and is performant, you may watch this video with the well chosen name "The Unreasonable Effectiveness of Multiple Dispatch" from the JuliaCon 2019: https://youtu.be/kc9HwsxE1OY?t=67
If there are any (sudden) questions coming up, feel free to ask! Hopefully overloading or the literally more powerful multiple dispatch will rise 😇

from language.

rapus95 avatar rapus95 commented on May 29, 2024

throw everything at the JIT and let it specialize the world

technically speaking, IIRC that part still happens even bevor LLVM gets into play. Many parts of the optimization happen from within julia code (like the inference and constant propagation step). So, as long as there is a code representation within the language, that part can be lifted from the compiler into macros and similar reflection-type stuff. (Sure, bootstrapping gets non-trival that way)
Is "code as first class citizen" something that fits to Dart?

OTOH AOT specialization (similar to c++ templates) would be ok, wouldn't it?

from language.

munificent avatar munificent commented on May 29, 2024

(Sure, bootstrapping gets non-trival that way)
Is "code as first class citizen" something that fits to Dart?

No, it's definitely not that dynamic of a language these days. Think of it as in a similar bucket with Go and C++.

OTOH AOT specialization (similar to c++ templates) would be ok, wouldn't it?

In principle, yes. I don't know if doing compile time specialization with runtime dispatch is a combination that's been explored before, though.

from language.

Hixie avatar Hixie commented on May 29, 2024

I've been teaching myself Kotlin and Swift recently and they have changed my feelings about this issue quite strongly.

In both cases, I kept running into the same issue: I would try to use a method from a library and try to rely on the IDE/compiler to tell me what arguments to provide and what types they should be and so forth. The method name I got from autocomplete, often, or by guesswork. This approach works great with Dart. You think you want to use methodFoo, so you type that, and then the analyzer says "hey, I need 3 positional arguments", and then you give one and it says "that argument should be an int, not a String", and so on, until you've got code that compiles.

With both Kotlin and Swift, this doesn't work. You type in the method name, and it tells you it's missing an argument. You provide the argument, and now it says it's the wrong type. You try to fix the type, and it just says there's no matching overload. The IDE/compiler can't help you because it doesn't know which of the two (or dozen) overloads you might want, and any time it tries to guess it guesses incorrectly. It's a constant battle.

from language.

jodinathan avatar jodinathan commented on May 29, 2024

@rrousselGit I think you attached the wrong issue

from language.

rrousselGit avatar rrousselGit commented on May 29, 2024

Ah we aren't on dart-lang/language, my bad
Fixed

#836

from language.

jodinathan avatar jodinathan commented on May 29, 2024

I don't know much about NNBD semantics and features but thinking about your example @rrousselGit, maybe NNBD should be shipped with method overloading.
It will indeed lead to some "Type method()" "Type? methodNull()"

from language.

Hixie avatar Hixie commented on May 29, 2024

I was running into things like methods that take either two strings or two integers, and I give the method one string and one integer, and the error messages are unintelligible, because they either pick one of the methods (reliably not the one I think I want) and tell me to change the "right" argument to be "wrong", or they just tell me about all the variants but that ends up being a disaster because compilers always think types are more complicated than I do (e.g. because of multiple nested generics being involved, or function types being involved) and so the list is incomprehensible, or they just give up and tell me the method doesn't exist, or... There are definitely cases where it does the right thing, but the number of cases where it just makes the development experience less pleasant was surprising to me.

from language.

rapus95 avatar rapus95 commented on May 29, 2024

@Hixie well, then again, we're at the problem of good suggestions. In the first place, as Dart has strict typing, it should follow a strict applicability rule (i.e. if the call would be ambiguous, error).
Again, referring to Julia, when you try to call a method there with arguments that would fit equally well into multiple definitions, then it errors with "method ambiguous" and shows the interfering definitions (which usually are good suggestions since these are the most specific ones that fit your call arguments). Generally, defining the method with a manual dispatch based on the actual leaf types (as opposed to abstract/subtypable types) will resolve such ambiguities by providing a single best fitting function.
Either way, whenever you have the possibility to overload a function/define multiple variants of it, there needs to be a strong emphasis not to literally "overload" that given function identifier. Otherwise you'll run into many ambiguities. In Julia we coined "type piracy" as the name for certain overloads that you shouldn't do because those lead to these exact ambiguities and other weird behaviour (which is still well defined but mostly unexpected)

My biggest issue with the methodFoo vs methodBar practice is, it's very hard to give a proper name to a variant.

In the worst case it even leads to "encode my signature" style where you just paste the type names into the function signature. Common cross-language examples are:

  • List.fromArray, List.fromStream, List.fromanyotheriterable
  • generally any other type which can have multiple individual implementations for similar concepts (=same abstract interfaces) when creating a unified object. since it encodes the same abstract information in different implementations)

Opposed to that, there's also stuff like

  • Vec2.fromCartesian, Vec2.fromPolar etc
    where multiple different abstract meanings are represented by the same types (here, floating point tuples). In those cases having all conversions behind the same identifier is not possible and would be bad style anyway since you would link different abstract behaviour to the same identifier. bad idea. makes overloads ugly, complex and anything but elegant. There the current approach is actually a quite beautiful fit to encode abstract meaning of the arguments. (On the other hand, if you had multiple types for Cartesian and Polar representation which both implement some Vec2 interface, having to define .fromX everywhere would be bad style again, since the argument type already holds all information.

Overall the primary advantage of having arbitrary overloads is that you aren't dependent upon the library author to define an applicable interface in order to participate in their ecosystem.
If they have a function which "doesX" and you have a type that also has some way to be changed in the same abstract meaning as "doesX" works, then you just add your own definition for your own type to that abstract meaning of "doesX". And suddenly your type can participate in everything that relies on being able to do X.
It's some sort of automated single function interface and quickly escalates into (compile time) duck typing.

from language.

Hixie avatar Hixie commented on May 29, 2024

Again, referring to Julia, when you try to call a method there with arguments that would fit equally well into multiple definitions, then it errors with "method ambiguous" and shows the interfering definitions (which usually are good suggestions since these are the most specific ones that fit your call arguments).

That's the kind of thing I'm talking about. I found it surprisingly less pleasant an experience than having to pick from a list of "fromXXX" methods or similar.

from language.

rapus95 avatar rapus95 commented on May 29, 2024

a list of "fromXXX" methods or similar.

What's the difference between typing MyType.<Tab>

  • (x, y)
  • fromT1(x)
  • fromT3(x)
  • fromOther(x)

and typing MyType(<Tab>

  • (x, y)
  • (x::T1)
  • (x::T3)
  • (x)

?

In general, if the code follows appropriate design guidelines you shouldn't even have to care about the types since the constructor should be designed for all of them. If not, you have something which doesn't have a canonical transformation rule. And if you can't distinguish by types (=no canonical transformation) having precise names is good style anyway. Language design cannot be blamed for bad programming habbits.

I was running into things like methods that take either two strings or two integers, and I give the method one string and one integer, and the error messages are unintelligible, because they either pick one of the methods (reliably not the one I think I want) and tell me to change the "right" argument to be "wrong", or they just tell me about all the variants but that ends up being a disaster because compilers always think types are more complicated than I do (e.g. because of multiple nested generics being involved, or function types being involved) and so the list is incomprehensible, or they just give up and tell me the method doesn't exist, or...

Can you give an example of such a function?

from language.

rapus95 avatar rapus95 commented on May 29, 2024

So IMO, the difference between overloads and fromX/fromY/etc. is that you get to select the overload yourself and force the compiler to give the correct error message for the one you want, rather than relying on it guessing and potentially giving a spurious message.

When the compiler guesses, something went wrong already. The compiler should say, that there's no matching function and offer a list of options. But for the example you referred to, I don't think, language design has to cover the case that people change their API. Follow SemVer & deprecation guidelines and the mentioned case shouldn't occur at all.
If you however have the opinion that language design should cover that case, how should the compiler resolve the situation when an author doesn't change types but the function name.

IMO the possibility of bad compiler messages shouldn't be a driving argument for the language design. Especially in a situation where the proposed language design CAN have exact messages.

No matter how many overloads you have, if you have a type combination which doesn't fit an existing overload you get an error. I don't see the difference between having overloading and not having overloading for the procedure of resolving wrong argument errors. In the one situation you look for a function name that represents the right combination of abstract meaning and argument types and in the other situation you look for the function name that fits your abstract meaning and then select the overload which fits your argument types. That's why I asked for an explicit example (where the result can only be flawed even if the author follows coding guidelines).

from language.

DanTup avatar DanTup commented on May 29, 2024

I don't think, language design has to cover the case that people change their API. Follow SemVer & deprecation guidelines and the mentioned case shouldn't occur at all

I don't think that's entirely the case. Even if you follow SemVer, I might update a package across several versions (expecting breaks) and then have to fix up calls. Or I might rebase a branch I'm working on where a colleague has changed an internal API (and fixed up all existing code) but my branched code is now invalid. There are many reasonable ways you can end up with compile errors due to incorrect arguments and the less friction there is in resolving them, the better IMO.

I don't see the difference between having overloading and not having overloading for the procedure of resolving wrong argument errors. In the one situation you look for a function name that represents the right combination of abstract meaning and argument types and in the other situation you look for the function name that fits your abstract meaning and then select the overload which fits your argument types.

One difference is that one probably has a meaningful name attached to that specific combination of arguments whereas the other one doesn't. It's much easier to read a few meaningful function names that have to scan a (potentially long) list of arguments (and although IDE-specific, most IDEs I've used can show multiple function names in a code completion list at once, but require you to toggle between overload info.. I'm not saying languages should have to make up for that, but I think it's a fair usability consideration).

I'm not against overloads - I gave an example for them too. I was just sharing that I'd seen the issue described above a few times recently (though I admit my example was manufactured because I couldn't remember/find the specifics where I'd hit them).

from language.

sodiboo avatar sodiboo commented on May 29, 2024

How about making sure every overload has a unique name? This would fix the issue with tearoffs

With matanlurey's example for a dice roll

abstract class DiceRoller {
  List<int> roll.parse(String expression);
  List<int> roll.many(int amount, int sides);
  List<int> roll.from(DiceRoll roll);
}

And then don't allow tearing off the method, only the full overload's name. The actual overload if accessed by a dynamic getter would be some automatically generated Overload class or whatever, and then it's not at all callable directly, only through the different overloads. When writing code with proper types, you can either do the same and call the full overload name, or if you just called the method name, it would be replaced with a specific overload at compile time.

void main() {
  var roller = DiceRollerImpl();

  var parsed = roller.roll("4d6"); // Replaced with roller.roll.parse("4d6"); at compile time
  var parsed2 = roller.roll.parse("4d6"); // Identical to the above, except slightly more verbose
  var many = roller.roll(4, 6); // Replaced with roller.roll.many(4, 6); at compile time

  var roll = roller.roll; // Error: Cannot tear off an overloaded method directly
  List<int> Function(String) roll2 = roller.roll; // Error: Not even unambiguos tearoffs

  var rollParse = roller.roll.parse; // List<int> Function(String)
  var rollMany = roller.roll.many; // List<int> Function(int, int)

  dynamic roller2 = roller;
  var parsedError = roller2.roll("4d6"); // Error: overload cannot be invoked dynamically
  var dynamicRoll = roller2.roll; // This is fine, simply has a couple methods on it
  var dynamicParsed = dynamicRoll.parse("4d6"); // Rolls fine at runtime
  assert(rollParse == dynamicRoll.parse); // Same method

  
}

Alternatively, overloads could be compiled with unique names instead of nesting under a single "overload object", but then getting that property dynamically might be a bit hard (though, to be honest, how useful is that anyways? tearoffs would still work the same given you have the correct static type)

Since each one does have a unique name and won't ever become a single object with multiple call signatures, this compiles fine to JS

Basically my idea is equivalent to this, except you can call DiceRollerRoll statically at compile-time with the full type benefits of having unique names, and at runtime there is no call signature for DiceRollerRoll, and DiceRollerRoll would be created implicitly by the compiler (perhaps extending from some kind of Overload type)

Really it is the same as just creating multiple methods with different names as a suffix, just without the ugliness that is almost certain to be produced from keeping them unique from everything else, and slightly less verbosity

abstract class DiceRoller {
  DiceRollerRoll get roll;
}

abstract class DiceRollerRoll {
  final DiceRoller roller;
  DiceRollerRoll(this.roller);
  List<int> parse(String expression);
  List<int> many(int amount, int sides);
  List<int> from(DiceRoll roll);
}

One issue i could see with this syntax is being ambigous with a constructor where the overloaded method shares the same name as the actual type, but just like set/map literals it could prioritize the constructor where valid (i.e. initializing formal parameters, an initializer list, or redirecting to a constructor, aswell as where the body would initialize all non-nullable non-late fields) otherwise try an overload with dynamic return, or maybe overloads would need a return type to be recognized (that way, you don't conflict with constructors, and as a bonus you don't have an implicit dynamic return type), or simply disallow overloading the same name as the class to avoid this problem altogether?

Operators

Personally i don't actually think method overloading would add that much to the language (though it would be nice), but i would really want to overload multiple signatures of the same operator, if you look at the implementation of + for int, you'll find it inherits from num, thus num operator +(num other) is the signature - however, int + int evaluates to an int, even though that's not what the operator says it does, almost as if int has 2 signatures for +: int operator +(int other) and num operator +(num other). This makes sense, because if you add 2 integers, you won't get a fractional value, thus it's safe to say it's an integer. This isn't possible to implement in pure dart, this is simply a part of the type system and int being one of the core types, so therefore it gets special treatment, just like num is really a union of int|double, FutureOr is literally just a union type, and bool can't be extended or instantiated, it's just the type shared by 2 objects, all 3 of these examples make perfect sense, but are also impossible to implement in pure dart in the same way.

I'd imagine that for operators, there might not be as much overhead for multiple implementations based off the signature, since they're always restricted to a single required parameter, and overload resolution could essentially be simplified to dynamic operator +(dynamic other) => implementations[other.runtimeType](other); where implementations is a map, of course then i guess it's based on the runtime type and not the static type, so if an implementation used num it couldn't take an int, so obviously something a little more sophisticated, but certainly a hell of a lot faster than needing to look up multiple parameters with signatures, aswell as optional parameters which more or less conflict with overloading, cause if you have range(int min, [int max = 100]) and range(int max), which one is called with range(3)?

I'd imagine that statically, it should look for the closest type in the supertypes of the operand's type (i.e. first checking if anything takes int, then int?, then num, then num?, then Object, then Object?, and finally dynamic, and if the operand is Never then no implementation needs to be chosen at all), and dynamically (for faster resolution), it could look for in some given order the first implementation that supports the runtime type of the object (perhaps simply the order it's written in the class with subtypes being before their supertypes, and something more like operator +(other) => implementations.firstWhere((impl) => other is impl.operandType, orElse: () => throw TypeError());)

With multiple operator overloads per operator, equality would be less of a pain to implement

class Foo {
  bool operator ==(Foo other) => true;
}

void main() {
  assert(Foo() == Foo());
  assert(Foo() != Object()); // Inherited from Object, and these are not the exact same instance
}

and i'd also love to be able to add extension operators that are already implemented in the class, for other types, but that isn't possible right now

extension BigIntArithmetic on int {
  BigInt operator +(BigInt other) => BigInt.from(this) + other;
}
extension IntArithmetic on BigInt {
  BigInt operator +(int other) => this + BigInt.from(other);
}

void main() {
  print(BigInt.one + 1); // 2
  var x = BigInt.one;
  x++; // Incrementing on BigInts! heck yeah!
  print(x); // 2
}

Maybe a restriction for this would be that you can't overload for a subtype of what's already overloaded in extension methods? but a really great use case would be defining custom equality rules for a class that has never heard of your library via extension methods, and that would break this use case, albeit only for the == operator specifically - maybe an alternative could be allowing extension methods to add another implementation for an operator if, and only if, the only operator in the superclass that supports the type is of Object, and the given type will always return false in that implementation, a common pattern in equality operators is to check for each type before doing anything, otherwise returning false, so in a chain of such type checks, if the extension's operand type skips all of them and will be false, the overload is accepted for an extension

class Foo {
  final num value;
  Foo(this.value);
  bool operator ==(Object other) {
    if (other is num) return value == other;
    return false;
  }
}

extension FooExtensions on Foo {
  // Allowed: the catch-all implementation can only ever return true if it has a num, which a BigInt isn't
  bool operator ==(BigInt other) {
    return BigInt.from(value) == other;
  }
  
  // Error: int is a num, and a num could potentially return true in the catch-all, so this is *not* allowed
  bool operator ==(int other) {
    return value == other;
  }
}

class SubFoo {
  // On the other hand, this is completely fine, because it's overridden in a subclass, and as such doesn't change the behaviour of Foo based on what extensions you import...
  bool operator ==(int other) => value == -other;
}

A big benefit i see is any numerical type could support the increment and decrement operators aswell as being able to add its own type with the + operator, and maybe just in case this is the only reason it needs to support int, there could be some annotation that adds a warning to any code using the + or - operators with an int operand, instead of with ++ or --

from language.

jodinathan avatar jodinathan commented on May 29, 2024

Is this at sight?

All this nonNull versions of stuff is really boring and makes the code sparse and ugly.

When using graphql, which we use a lot, most of things can be null and that is the point of its flexibilization. But we could still have sound stuff.

For example in our listing function you can fetch the totalCount of items and that would be a count() in the database, which is expensive. So it makes a lot of sense to be optional, thus, null.

We already use builders extensively and it makes sense for us to create several optimized methods that do the same but with different approaches or results.

With the advent of static meta programming the source generated stuff shall increase even further.

from language.

lukepighetti avatar lukepighetti commented on May 29, 2024

Currently, a Dart object has at most one accessible member with any given name. Because of that, you can do a tear-off of a method. If you could overload methods, tear-offs would no longer work. You would have to say which function you tore off, or create some combined function which accepts a number of different and incompatible parameter signatures. It would make dynamic invocations harder to handle. Should they determine that method to call dynamically? That might cause a significant code overhead on ahead-of-time compiled programs.

Is there no clear way to have the compiler use it's knowledge of types to route a tearoff to the correct method based on the types associated?

abstract class Foo {
  static void hello() { }
  static String hello() => 'hello';
}

main(){
  final String Function() secondHelloAsTearoff = Foo.hello;
  print(secondHelloAsTearoff); // 'hello'
}

from language.

lrhn avatar lrhn commented on May 29, 2024

@lukepighetti Answer: ... maybe!

Using the context type may allow a tear-off to be chosen from the known members of that name.
For example:

class C {
  int foo(num x) => x.toInt();
  int foo(int x, int y) => x;
}
void main() {
  int Function(int) f= C().foo;  // chooses unary `int foo(num)`.
}

The problems comes when you have partial information available.

class D extends C {
  int foo(int x) => x;
}
void main() {
  C c = D();
  int Function(int) f = c.foo;
}

First question is whether int foo(int) is a different function than int foo(num)?
It's not a valid override, so it must be!
But(!) if the two were declared in the opposite order, would they still be different functions or would it be a virtual override?
(Whoops, what does overloading even mean in a language with covariant overriding? And optional parameters! That's not clear at all.)

Second question is whether the tear-off c.foo would choose the more precise foo from D, or the foo it knows is there from C? That is, is tear-off statically decided or dynamically decided?

Likely statically decided. That's definitely the safest approach. Which means needing to know the static type you're tearing off from, so what about tear-offs from dynamic.

void main() { 
  dynamic c = D();
  int Function(int) f = c.foo;
}

Does that even work?
It does now, but we could say it doesn't work if foo is overloaded at all.
Or we could dynamically try to find the best version for the context type.

And what about extension methods. Should we choose a more precise extension method over an instance method? (We will just have to make a decision, but likely yes.)

All in all, I think context type based tear-off selection is possible. It might not always be predictable, but that's because we don't have a real model for what overloading means yet. We might need a C#-like new/override marker to distinguish a potential override from an overload:

class C {
  int foo(int x) => x;
  int bar(int x) => x;
}
class D extends C {
  override int foo(num x) => x.toInt();
  new int bar(num x) => x.toInt();
}
void main() {
  C c = D();
  int Function(int) f1 = c.foo;  // D.foo by virtual lookup
  int Function(int) f2 = c.bar; // C.bar because D.bar is not the same function.
}

Decisions!

from language.

lrhn avatar lrhn commented on May 29, 2024

Yes, C# is the reason I think it can possibly work, but the reason their overloading works is an intricate set of decisions throughout the C# design. Tear-offs work too, but that's because they can already do type based resolution of overloaded methods.
If we can get overloading working, I think we can get tear-offs to work too. Getting overloading working is non-trivial.

Where Dart differs in a significant way is our optional parameters, which is really the Dart approach to overloading: One function declaration with multiple (related) signatures.

C# allows parameters to have default values, but that makes the argument optional, and the default value is inserted statically at the call point. A function like void F1(int a, int b = 0) is a binary function which you can call without providing the second argument. If you tear it off as a delegate like public delegate void Binary(int a, int b = 7);, then calling that delegate with one argument makes the second argument be 7.
You cannot assign F1 to a public delegate void Unary(int a); delegate type, it's inherently a binary function, it must be called with two arguments (one of which may be supplied by the compiler).

Dart's function subtyping, where we allow a void Function(int, [int]) to be a void Function(int), might get in the way of overloading.

from language.

nex3 avatar nex3 commented on May 29, 2024

Given that optional arguments act like overloads under some circumstances, one way to make the two play nicely would be to be fairly strict about their interactions—for example, I think it would be easy for a user to understand that void Function(int) and void Function(int, [int]) can't both be defined for the same class.

from language.

Levi-Lesches avatar Levi-Lesches commented on May 29, 2024

Combining your two comments, how about a rule that you cannot overload a member a with a subtype b, because in every instance where you can use b, you can already use a, so the compiler wouldn't know which one to use.

from language.

lukepighetti avatar lukepighetti commented on May 29, 2024

I'm not sure what the current rule is, but it sounds like types/parameters are part of the signature, but you cannot have name collisions. What would happen if we removed the unique name requirement and did nothing else?

from language.

jodinathan avatar jodinathan commented on May 29, 2024

@munificent two things:

  1. Dart already has overloading in the try catch clause. If overloading is not something necessary, don't you agree that all catch should be something like catch(Exception e, [StackTrace st])?

  2. Is there any plan to not have these many somethingOrNull like firstWhereOrNull without losing performance? I mean that with overloading the compiler do a check and call the correct method by the type signature so we don't have to do it at runtime.
    Also a simple rule that we have here is not to have methods with And or Or in the name. It usually means bad architecture and the whole context should be reanalyzed.

from language.

jodinathan avatar jodinathan commented on May 29, 2024

which I'd do in a heartbeat if I could migrate the world easily

The point is not if we can change that or not, but the reasoning behind not having an optional parameter in the first place.

Assuming that not requiring the StackTrace in the try catch clause can be seen as an optimization, since we are telling the compiler we don't need that info, makes total sense to it be overloadable as it currently is.

The point is simplicity. You don't want to see the StackTrace argument everywhere as you don't want to have firstWhereOrNull kind of method.

Be it parameter or return type, the point is overloading in general. However, if we could have at least the return type overloadable I would be glad, honestly.

from language.

jodinathan avatar jodinathan commented on May 29, 2024

For example, imagine parsing a DateTime. Right now there are several options: .fromMicrosecondsSinceEpoch, .fromMillisecondsSinceEpoch, .utc, .parse, and .tryParse. Sure these names are quite verbose, but imagine trying to figure out what value to pass if there were just one DateTime.parse constructor.

The only one of that list that I see using overload is parse.

The reason why we have firstWhereOrNull instead of tryFirstWhere is because when you type firstWhere you see the firstWhereOrNull in the list. It would be harder to find the nullable version if it was tryFirstWhere because of autocomplete.

I understand the point regarding the aesthetic preference but think with me a bit and take the tryParse above as example.
Coding some project that uses other libs we know that there is a something method, now we need its nonnull version. What is easier?

a) a method that is overloaded to nullable and nonullable versions that you can read the docs and the implementation in the same place
b) some random name that is up to the developer like trySomething, somethingOrNull or nonNullSomething

from language.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.