Skip to content

Support method/function overloads #1122

Open
@nex3

Description

@nex3

This has been discussed periodically both in the issue tracker and in person, but I don't think there's a tracking issue yet. (EDIT: Here is the original issue from 2011 - dart-lang/sdk#49).

Now that we're moving to a sound type system, we have the ability to overload methods—that is, to choose which of a set of methods is called based on which arguments are passed and what their types are. This is particularly useful when the type signature of a method varies based on which arguments are or are not supplied.

Activity

jodinathan

jodinathan commented on Sep 29, 2017

@jodinathan

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

lrhn

lrhn commented on Jun 22, 2018

@lrhn
Member

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.

jodinathan

jodinathan commented on Jun 22, 2018

@jodinathan

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

eernstg

eernstg commented on Jun 22, 2018

@eernstg
Member

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.

matanlurey

matanlurey commented on Jun 26, 2018

@matanlurey
Contributor

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

matanlurey

matanlurey commented on Jun 26, 2018

@matanlurey
Contributor

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

eernstg

eernstg commented on Jun 29, 2018

@eernstg
Member

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

matanlurey

matanlurey commented on Jun 29, 2018

@matanlurey
Contributor

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

nex3

nex3 commented on Jun 29, 2018

@nex3
MemberAuthor

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.

matanlurey

matanlurey commented on Jun 29, 2018

@matanlurey
Contributor

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.

nex3

nex3 commented on Jun 29, 2018

@nex3
MemberAuthor

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.

matanlurey

matanlurey commented on Jun 29, 2018

@matanlurey
Contributor

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.

nex3

nex3 commented on Jun 29, 2018

@nex3
MemberAuthor

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.

matanlurey

matanlurey commented on Jun 29, 2018

@matanlurey
Contributor

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.

munificent

munificent commented on Jul 4, 2018

@munificent
Member

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

123 remaining items

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    requestRequests to resolve a particular developer problem

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @nex3@munificent@matanlurey@RdeWilde@yjbanov

        Issue actions

          Support method/function overloads · Issue #1122 · dart-lang/language