-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support method/function overloads #1122
Comments
any news on this? |
Not in Dart 2. 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. |
but with a sound dart we don't have dynamic invocations, do we? |
We can certainly still have dynamic invocations: If you use the type 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 |
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.
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) ProposalDon'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 unknownRather, 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 Side notesLet's consider how users are working around this today:
class Foo {
void bar([dynamic nameOrNumber]) {
if (nameOrNumber == null) {
print('bar()');
return;
}
if (nameOrNumber is String) {
// ...
return;
}
if (nameOrNumber is num) {
// ...
return;
}
}
}
class Foo {
void bar() {}
void barString(String s) {}
void barNumber(num n) {}
}
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 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 |
By the way, this would have solved the class Future<T> {
Future<T> catchError(Object error) {}
Future<T> catchError(Object error, StackTrace trace) {}
} ... as a bonus :) |
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 ;-). |
Why? If we just don't allow dynamic invocation to invoke static overloads, nothing is needed.
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). |
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. |
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. |
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. |
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. |
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 |
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. |
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:
|
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" 😄 |
@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? |
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 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 For instance, we always know everything about the type of a function literal at the call site. Special types like 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
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. |
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. |
@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 think dynamic invocation is red herring. C#'s (We don't have implicit conversions in Dart yet, but we will once you can pass 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>();
} |
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) |
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. |
Thanks for this! A few more questions, but don't feel like they are important to answer immediately :)
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:
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.
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) {}
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) {
// ...
} |
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.
I don't think that's the cause of much of the pain.
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.
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.
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.
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. :) |
Thanks! I am sure I will understand this issue eventually :)
Did you mean one of the key uses of extension methods, or overloading? |
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. 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. 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 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 Anyway, this is about overloading, not extension methods. |
Would it make it easier if it is available only for positioned parameters?
From my experience, overloadable methods are normally very well defined and with few parameters.
Atenciosamente,
Jonathan Rezende
… Em 9 de jul de 2018, à(s) 06:53, Lasse R.H. Nielsen ***@***.***> escreveu:
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.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub <https://github.com/dart-lang/sdk/issues/26488#issuecomment-403424824>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AF6Y3jQTXj788r5T9g9c8q-24N1wE3Qaks5uEygbgaJpZM4IhzBM>.
|
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. |
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 |
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? |
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!"); |
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-offsPresumably 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 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 Overriding and parameter typesConsider: class A {
void foo(int i) {}
void bar(String s) {}
}
class B {
void foo(num i) {}
void bar(bool b) {}
} Is 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 Optional parametersConsider: 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? PromotionConsider: 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 InferenceConsider: class C {
void method(List<int> ints) {}
void method(List<Object> objects) {}
}
main() {
C().method([]);
} What does this do? GenericsConsider: class C<T> {
method(num n) {}
method(T t) {}
} Is that a valid pair of overrides? What if 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 I'm sure it gets even weirder when generic functions come into play. CovariantHere'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 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. |
@munificent two things:
|
Catch clauses are not methods. Yes, it would be nice to have a way to pass either For Even then, the difference between |
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 The point is simplicity. You don't want to see the 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. |
@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.
I agree with this but the ideal answer, IMO, would be to make
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 For example, imagine parsing a |
The only one of that list that I see using overload is The reason why we have I understand the point regarding the aesthetic preference but think with me a bit and take the a) a method that is overloaded to nullable and nonullable versions that you can read the docs and the implementation in the same place |
How about
|
Using the context type to disambiguate the tear-off operation has been suggested. It differs from the other places where we currently use the context type to do something. In those, the context type can cause something to happen ( If we make (There are other issues with method overloading in Dart than tear-off resolution. For example, Dart's optional parameters is already a kind of overloading, which allows a single method to satisfy two different member signatures. When you combine those with actual overloading, and subclassing, it's non-trivial what it even means to override one or more superclass members.) |
On the topic of using a context type to disambiguate, there's also Crystal which requires you to specify the types of the parameters in order to tear off a method str = "hello"
proc = ->str.count(Char)
proc.call('e') # => 1
proc.call('l') # => 2 https://crystal-lang.org/reference/1.6/syntax_and_semantics/literals/proc.html#from-methods |
But in practise we advice making more members instead of using this kind of overloading, especially if we can't statically enforce the right arguments void foo({required A a, B? b, C? c}) {
if(b != null && c != null) throw ArgumentError('Provide at most one of b and c.');
// ...
} We usually rewrite that to two methods (which have overlapping behavior if neither b nor c are provided). Now, if we could write some kind of more fancy constraints of permutations of optional parameters that are correct that would work with static checking ... 🎉 |
An approach occurred to me that might at least handle the overload/override ambiguities, tear offs, and mitigate the possibility of unintentional breaking changes - named overloads: class A {
//We name each overload at the declaration site.
String foo.withInt(int x) => "int (A)";
String foo.withString(String x) => "String (A)";
String foo() => "Nothing"; //Not allowed, name collision on "foo" and can't be an overload of it without a name.
}
class A1 extends A {
//To override one, you must use the same name.
@override
String foo.withString(String x) => "String (A1)";
//To make a new overload in a subclass, just use a new name.
String foo.withNum(num x) => "num (A1)";
}
void main() {
var a = A1();
//The name is not required at the call site.
print(a.foo(1)); //int
print(a.foo("hello")); //String (A1)
print(a.foo(1.0)); //num (A1)
//A specific overload can be referenced by name, which is used to perform tear-offs safely, or force a particular overload.
var bar = a.foo.withInt;
print(bar(1)); //int
print(a.foo.withNum(2)); //num (A1)
} Requiring each overload to have a unique label helps with declaration ambiguities while letting the variants share a singular name at the call site. It would also prevent people from accidentally overloading something they wanted to override, or vice versa. I do realize the silliness in solving a chunk of the problems with overloading by giving the overloads distinct names - i.e. missing the whole point of overloading. But in practice it seems like only a slight sacrifice in convenience assuming you write a method once then use it many times going forward. This doesn't address all the problems, of course. Dispatch in particular would still have some pretty glaring questions, and I'm not really convinced that this feature would be worth the trouble in the first place. But I wanted to jot down this line of thought and see if it goes anywhere when fleshed out. Going down the above (not comprehensive) list of things that would need to be solved, it at least provides solutions for these dilemmas:
Since tear offs have to be manually disambiguated, adding a new overload with its own label won't affect existing tear offs. It would be a breaking change to make a non-overloaded method into an overloaded one, but the change in declaration syntax should make that obvious enough. You could maybe add a way to blindly tear off an overloaded function and then resolve it at the call site - either storing it dynamically and letting the user assert that it'll work at runtime, or keeping it in a special function type that remembers the original overload family as part of its static type - but that seems tricky either way.
class A {
void foo.a(int i) {}
void bar.a(String s) {}
}
class B {
void foo.a(num i) {} //Valid override
void bar.a(bool b) {} //Invalid override, since String isn't a subtype of bool.
void bar.b(bool b) {} //Valid new overload, not an override.
void bar.a(Object b) {} //Valid override
}
class A {
void method.a(num n) {}
}
class B extends A {
void method.a(covariant int i) {} //Valid override
void method.b(Object? o) {} //Valid new overload
} That doesn't answer the question of which method a double would dispatch to, especially if the static type is A but the runtime type is B. Similar complications remain with generic parameters and dynamic invocation, and I've barely given any thought to how return types work with all this, so I'm inclined to just leave it at that for now. |
The extra name sounds like multiple different-named methods which allow being called through a (shared) prefix of the name, using some sort of static resolution. There is no real overloading, there are just longer names and static dispatch to one of those longer names when you only specify a prefix of the longer name. You can add more members with the same prefix in a subclass. Static resolution won't see them when calling on the superclass. And you can always call a specific method directly. Overloading an existing member gets weird. If you have a In any case, it's like The thing you get for free is that invoking that object as a function will statically decide which of the members is called. Tear-offs can be decided the same way as invocations, using a context type's parameter types to choose instead of actual argument types. If there is no useful context type, it's an error. This has all the usual problems with It also suffers from the same kind of name conflicts as Dart does today. If you add |
The way I was imagining it, it'd be an error to have an overload syntax
Ah, I figured once return types were considered the whole thing would snag pretty fast. I was sorta leaning towards dynamic invocation where you resolve all the runtime types before picking a method. I had a hard time imagining how you'd be able to statically handle the covariant situation, generics, and dynamic parameters. But I guess that would just propagate the problem outwards. Even if you computed a least upper bound for all the possible return types (and good luck when subclasses can add new ones), it'd still make it a strongly breaking change to introduce new overloads, which was something I was hoping to avoid. Losing static analysis for the whole feature would pretty strongly tip the "is it worth the trouble" scales towards the "no" side. |
While I was creating Typings I had to deal with transpilling overloaded methods from TS to Dart. As JS interop uses extension methods a lot, I thought of a solution for tearoff and also bad overloading usage: Allow overloading methods only in extension methods.So a class: class Foo {
void bar();
} Could have extension OverFoo on Foo {
void bar(String daz); // ok
} Then you can easily catch the final foo = Foo();
final v1 = foo.bar; // void bar() {}
final v2 = (foo as OverFoo).bar; // void bar(String); With this approach you can statically know the correct callback while also putting a minor restriction to its usage without really restricting it. It would also solve one the JS interop questions |
Allowing overloading only using extension methods was actually something I intentionally tried to prevent with the extension methods design. It was a goal (not completely reached, but still an aspiration) to not make extension methods better than instance methods. If we want to allow overloading, we want to allow overloading on both instance methods and extension methods, so the choice between instance and extension method is made on the basis of which best fits the use case, not which of them happens to allow overloading. Anyway, if we were to allow overloading on extension members, then we'd still infer the type of the receiver, then check which extensions apply to that, and whether they have one (or now more) members of that name. Then, most likely, instead of just picking the best But nothing about that is special to extension methods, it applies equally well (and badly) to instance methods, Maybe that choice in type inference was a bad idea, but it's not going to be easy to change. In short: Nothing here is unique to extension methods. Anything that can be used to resolve overloading involving extension methods can also be used to resolve overloading involving instance methods as well. And it has all the same problems which make that particularly hard for Dart. (Also, |
I thought the motives for not having overloading would be
That is why I thought overloading through extensions would be a help there.
This would not be an easy change, however, doesn't the new inference system already crawl the method beforehand? Consider: enum SomeEnum<T> {
a<int>();
const SomeEnum();
}
K foo<K>(SomeEnum<K> bar); Then when you use final k = foo(SomeEnum.a); // k is an int |
Type inference of
So, type analysis finds the static type of expressions. Sometimes those types include yet-unbound type variables. That's all well and fine, but it relied on having the full function type of Now imagine there being multiple possible functions that int foo(SomeEnum<int> a) => 9;
T foo<T>(SomeEnum<T> a) => a.value;
String foo(Object? a) => a.toStrung(); For each one of these, type analysis and inference can proceed, and they're all valid. But the return type differs. Then make it even harder, by not having an argument type which doesn't require any inference. Final<int> foo(Future<int> f) => f;
Final<double> foo(Future<double> f) => f;
/// and
final k = foo(Future.value(0)); Now it gets ambiguous. With only final Future<int> k = foo(Future<int>.value(0)); With only final Future<double> k = foo(Future<double>.value(0.0)); (Notice the meaning of the decimal literal depends on context type too!) With both in scope, which one should be chosen? Either works, neither is more or less specific than the other, |
We could be restrict those with an analyzer error |
We could, but what would the criteria be for deciding whether two declarations introduce ambiguity or not? I'm pretty sure I can always create an argument expression which depends on the context type, and do so in a way that makes it necessary to know the parameter type before doing type analysis on the argument expression. The way inference works, just a T hack<T>(List<T> list) => list.first;
// ...
anyFunction(hack([null])); will use the context type So, the only safe criterion for not causing a potential conflict is that the functions have completely incompatible argument shapes. And then we're back to not having type-based overloading, only shape-based. |
Would shape-based be number of arguments? Quick thinking on how to try to fix the ambiguity problem... I would take two approaches, first would be at definition by flatting all types and check if they collide: void foo<T>(T bar);
void daz<K>(K yum);
// flatted:
void foo(dynamic bar);
void daz(dynamic yum); // this throws an ambiguity error Then at usage level we must make sure that there is only one definition from types analysis. Final<int> foo(Future<int> f) => f;
Final<double> foo(Future<double> f) => f; Usage: final k = foo(Future.value(0)); // as the analyzer found two possible methods,
// it throws an ambiguity error
// the user could possibly fix this by making sure the analyzer know all types:
final k = foo(Future.value(0) as Future<int>); |
You get the same issue with just void foo(int x) {}
void foo(double? x) {} Those are completely disjoint parameter types, no generics, no shenanigans. Again, then I just do: T hack<T>(List<T> l) => l.first;
foo(hack([null])); and you still can't tell me whether that program is correct or not, or which To choose the function based on the argument type, you need to know the static type of the argument expression. So you cannot choose the the function to call based on the argument type. We could choose to not provide the argument expression with a context type when there are multiple possible functions to call. That's effectively what you suggest when you say that you have to make sure the analyzer knows all types - disable type inference. It's not even as bad as that, just not giving a context type still allows all the other type inferences to apply. We could select the possible functions ones which have a viable parameter list shape, and find the least upper bound of the parameter types of all those functions, and use that as context type. (Remembering that least-upper-bound is a fickle and confusing operation that can easily end up giving you effectively no context type anyway, especially for overloading where the argument types are more likely to be unrelated than not. So, probably not better than just having no context type.) But if we do that, adding an overloading would worsen type inference for everybody. You'd be better off just giving the function another name. (And then we're back to square one, which is probably why we've been here for a while.) |
I'm pretty sure that languages that support var x = foo and anonymous methods with overloads are going to push the compiler to algorithms that are in NP-Complete and then would just be too slow. Kotlin, Swift, Modern C#, Modern Java aren't known for quick builds. In the context of business with large orgcharts, managers will start assigning costs to builds I'd argue it's better if those are faster than slower. As stated above, in the context of the call site, how would the compiler be able to quickly determine what methods to call, normally there's an entire express tree, not just one method? If people are going to basically not have any kind of type inference, the developers type in all of the types, including the type of the output parameter then it's probably not a big deal. Developers like to type in extremely literal types, especially with generics, so the staff may not want to type in really large type definitions repeatedly and then folks start getting into type inference algorithms. There's physical limits to such things and tradeoffs. I guess it just depends. If someone knows of a system that has quick inference and build times and also allows methods with the same name and same number of parameters, please let me know. https://web.cs.ucla.edu/~palsberg/paper/dedicated-to-kozen12.pdf |
I wanted to share a more distinct example to highlight that Dart lacks support for implementing the same generic interface multiple times with different type parameters: using System;
interface IProcessor<T> {
public string Process(T input);
}
class MultiProcessor : IProcessor<int>, IProcessor<double> {
public string Process(int input) {
return $"Processing integer: {input}";
}
public string Process(double input) {
return $"Processing double: {input:F2}";
}
}
class Program {
static void Main(string[] args) {
MultiProcessor processor = new MultiProcessor();
// Explicit interface usage
IProcessor<int> intProcessor = processor;
IProcessor<double> doubleProcessor = processor;
Console.WriteLine(intProcessor.Process(42)); // Output: Processing integer: 42
Console.WriteLine(doubleProcessor.Process(3.14)); // Output: Processing double: 3.14
}
} Not sure how frequently this capability is used, but it’s a funny/notable feature that C# supports and Dart currently does not, especially without method/function overloading. |
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.
The text was updated successfully, but these errors were encountered: