dart-lang / language

Design of the Dart language
Other
2.65k stars 201 forks source link

Support method/function overloads #1122

Open nex3 opened 8 years ago

nex3 commented 8 years ago

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 - https://github.com/dart-lang/sdk/issues/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.

rrousselGit commented 5 years ago

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

nex3 commented 5 years ago

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.

rrousselGit commented 5 years ago

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
nex3 commented 5 years ago

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

rrousselGit commented 5 years ago

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.

ORESoftware commented 5 years ago

@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?

rrousselGit commented 5 years ago

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 {

}
rapus95 commented 4 years ago

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 😇

munificent commented 4 years ago

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.

rapus95 commented 4 years ago

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?

munificent commented 4 years ago

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

Hixie commented 4 years ago

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.

rapus95 commented 4 years ago

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

rrousselGit commented 4 years ago

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 https://github.com/dart-lang/language/issues/836

jodinathan commented 4 years ago

@rrousselGit I think you attached the wrong issue

rrousselGit commented 4 years ago

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

https://github.com/dart-lang/language/issues/836

jodinathan commented 4 years ago

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()"

Hixie commented 4 years ago

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.

rapus95 commented 4 years ago

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

Opposed to that, there's also stuff like

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.

Hixie commented 4 years ago

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.

rapus95 commented 4 years ago

a list of "fromXXX" methods or similar.

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

and typing MyType(<Tab>

?

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?

DanTup commented 4 years ago

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.

rapus95 commented 4 years ago

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

jodinathan commented 4 years ago

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 =]

rapus95 commented 4 years ago

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.

DanTup commented 4 years ago

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

rapus95 commented 4 years ago

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.

sodiboo commented 3 years ago

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

passsy commented 3 years ago

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){ }
jodinathan commented 2 years ago

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.

leafpetersen commented 2 years ago

Is this at sight?

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

lukepighetti commented 2 years ago

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'
}
lrhn commented 2 years ago

@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!

munificent commented 2 years ago

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.

lrhn commented 2 years ago

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.

nex3 commented 2 years ago

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.

Levi-Lesches commented 2 years ago

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.

Ryacinse commented 2 years ago

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

eernstg commented 2 years ago

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.

subzero911 commented 2 years ago

+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!

lukepighetti commented 2 years ago

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);
  }
}
Levi-Lesches commented 2 years ago

@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 
  }
}
lukepighetti commented 2 years ago

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?

Jetz72 commented 2 years ago

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!");
munificent commented 2 years ago

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.

jodinathan commented 2 years ago

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

lrhn commented 2 years ago

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.

jodinathan commented 2 years ago

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.

Levi-Lesches commented 2 years ago

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.

jodinathan commented 2 years ago

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