Skip to content
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

Method Overloading #73

Closed
RossTate opened this issue Nov 26, 2011 · 9 comments
Closed

Method Overloading #73

RossTate opened this issue Nov 26, 2011 · 9 comments

Comments

@RossTate
Copy link
Member

So before I start discussing casting in #65, I wanted to know your reasons for not having method overloading, since casting is your primary workaround. I can think of a few, but I don't know which ones overlap with your reasons and which ones you wouldn't care about, which makes it hard for me to figure out solutions.

P.S. I'll probably play devil's advocate a bit so I can get a better sense of what y'all want.

@gavinking
Copy link
Member

Well, first there's the obvious complications when:

  1. you have "overlapping" subtypes as the parameter types: print(Object o) and print(String s),
  2. one of the methods is generic: print<T>(T t) and print(String s), or
  3. you have optional parameters: print(Object o, Format f=DefaultFormat) and print(String s).

These issues can be substantially avoided by placing clever restrictions on the declaration of overloaded methods (essentially requiring that the parameter types have empty meets, and using the upper bound of a type parameter as its "type", etc, etc).

But then there were two features of our type system that screwed this up. First:

  • Introductions circumvent the restrictions above, meaning that even if it looks like the methods are unambiguous where they are defined, they might not be on the client site.

Now, I suppose you could handle this by an error on the client side, and anyway we're starting to have some second thoughts about introductions. Finally:

  • A method reference to an overloaded method is ambiguous and therefore we can't assign it a unique principal type without the need to inspect its surroundings.

This is the real major problem. Consider:

value f = print;

does f have type Callable<Void,Object> or Callable<Void,String>?

@gavinking
Copy link
Member

I guess you could say that we don't support overloaded methods for exactly the same reason that we don't support overloaded attributes.

@RossTate
Copy link
Member Author

you have "overlapping" subtypes as the parameter types: print(Object o) and print(String s)

I'm guessing the problem with this is the difference between static and dynamic typing. Java uses the static type of the argument to determine whether to call print(Object) or print(String), which means a String instance can be passed to print(Object) even though there's a print(String) defined. Indeed, I have seen bugs because of this.

one of the methods is generic: print<T>(T t) and print(String s)

Here I'm guessing the problem is ambiguity: print("blah") could go to either one and it's not clear which should be done. Similar problems happen if there's a print(Foo) and a print(Bar) and you call print(FooBar()).

you have optional parameters: print(Object o, Format f=DefaultFormat) and print(String s)

Optional parameters, as well as variatic methods, basically define method signatures at once, causing more opportunity for ambiguity.

So these three examples all deal with ambiguity of an invocation. Your last example has to do with ambiguity of a reference without invocation. This last problem is strictly more difficult than the first, and it doesn't seem like you have solutions to it for optional parameters, variatic methods, or generic methods. I'm gonna leave this one 'til later, cuz solving the first one solves another problem I'm worried about: operator overloading. (P.S. Operator overloading is simply the idea that an operator can be defined for multiple types; it is not the ability to define new operator symbols.)

Every now and then I ask people for feature requests in whatever languages they deal with. One reasonable request I get a lot is better handling of vectors and matrices. At the least, people want to be able to use standard operators so that they can write float * matrix * vector. This is not possible using Numeric or even if you changed * to use some Productable. The problem is float * matrix has the float on the left, and they can't extend Float to make it implement Productable<Matrix>. (Oh, and your Castable trick as a little broken cuz I can do 5.as<float|double>().)

It seems your goal is to have a definition for * that is unambiguous for each usage. This is mostly the same goal as having method overloadings that are unambiguous for each invocation. So a good solution for one will solve the other as well.

The best solution I've been able to come up with is to explicitly add disjointness to the language. Allow users to label classes and interfaces (such as Matrix) as disjoint. Then disallow a class/interface to extend two disjoint classes/interfaces where neither extends the other. You can also allow users to label invariant type parameters for classes/interfaces as disjoint, so that we can reason that Invariant<Foo> and Invariant<Bar> are disjoint for any non-type-variable Foo and Bar. You can also reason that Covariant<Integer> and Covariant<String> are disjoint since Integer and String are both disjoint. A nice thing about having disjoint be explicit is that it allows the user to communicate facts about the future: namely that two disjoint classes will remain disjoint in the future. This way disjointness won't accidentally prevent library structures from evolving.

Then clearly the next step is to only allow method and operator overloadings when the parameter types are disjoint from each other. This also allows the return types of the overloadings to depend more sophisticatedly on the parameter types, and gets rid of that of constraint on type parameters which has issues when the type argument is a union type.

There are more issues with overloading, but this is one important step so I wanted to see what y'all thought before moving on to other steps.

@gavinking
Copy link
Member

I'm guessing the problem with this is the difference between static and dynamic typing.

Yeah. I decided not to open up the whole issue of multimethods, simply because they just don't map nicely to Java bytecode, as far as I can see.

This last problem is strictly more difficult than the first, and it doesn't seem like you have solutions to it for optional parameters, variatic methods, or generic methods.

Exactly, it's really the problem of method references that convinced me to drop overloading. I don't want to have language features that simply don't work nicely together.

Operator overloading is simply the idea that an operator can be defined for multiple types; it is not the ability to define new operator symbols.

Sure, but we don't even allow you to define + for a different type. It's defined for Numeric and that's the end of the story ;-)

One reasonable request I get a lot is better handling of vectors and matrices.

Yeah, this one frankly bothers me. I would like to be able to do scale * location. But I don't like it enough to be willing to completely open up the language to all kinds of bizarre redefinitions of *.

(Oh, and your Castable trick as a little broken cuz I can do 5.as<float|double>().)

I don't think that actually breaks anything. A perfectly reasonable implementation of as() would be free to arbitrarily choose between Float and Double.

It seems your goal is to have a definition for * that is unambiguous for each usage.

No, beyond that, I'm of the view that if you're using multiple libraries that all define their own special meaning of *, the code quickly becomes very difficult to read. It's primarily a readability concern in the case of operators.

The best solution I've been able to come up with is to explicitly add disjointness to the language. Allow users to label classes and interfaces (such as Matrix) as disjoint.

OK, so this would be a nice way to be able to do what I described above as "essentially requiring that the parameter types have empty meets" but for interfaces as well as classes. I don't believe it to be necessary for classes, since you can always decide if they are disjoint or not by looking up the superclass hierarchy.

@RossTate
Copy link
Member Author

Sure, but we don't even allow you to define + for a different type. It's defined for Numeric and that's the end of the story ;-)

Do you mean *? I thought + was for any Summable?

No, beyond that, I'm of the view that if you're using multiple libraries that all define their own special meaning of *, the code quickly becomes very difficult to read. It's primarily a readability concern in the case of operators.

But it also improves the readability in many situations. My guess is more good programmers would appreciate having the syntax than would be discouraged by the bad programmers who misuse it. I'm not suggesting opening this up to all operators, probably just *, +, and - for now and then wait to see what the demands are.

Regardless, if you want it to be possible to do this for +, then you should remove Summable and Numeric as they're not compatible with such a feature. There's no reason to connect operators to interfaces if you don't intend them to be extended by users.

OK, so this would be a nice way to be able to do what I described above as "essentially requiring that the parameter types have empty meets" but for interfaces as well as classes. I don't believe it to be necessary for classes, since you can always decide if they are disjoint or not by looking up the superclass hierarchy.

I don't like the idea that I could strengthen the inheritance hierarchy in my library and by doing so unknowingly break someone else's code that mistakenly assumed two currently disjoint classes would forever be disjoint. By requiring disjoint to be explicit, you give library writers a way to specify "these will forever be disjoint" and not have to worry about others misusing temporary information.

@gavinking
Copy link
Member

Do you mean *? I thought + was for any Summable?

yes, sorry.

But it also improves the readability in many situations.

If you can trust the other developers in the ecosystem to not use * for unrelated things. And to not start defining operators named ~=~>. When I first started work on Ceylon, it was just a guess that you would not be able to. But now the empirical evidence on this issue is far more complete and it's very clear that you can't.

There's no reason to connect operators to interfaces if you don't intend them to be extended by users.

I want people to be able to add Complex. I want to be able to define Decimal and Whole, either in the language module, or perhaps in a separate ceylon.math module.

I don't like the idea that I could strengthen the inheritance hierarchy in my library and by doing so unknowingly break someone else's code that mistakenly assumed two currently disjoint classes would forever be disjoint.

Show me the specific example you're thinking of.

@FroMage
Copy link
Member

FroMage commented Dec 6, 2011

I have to add that I agree with Gavin when he said that we don't allow method overloading for the same reasons we don't allow attribute overloading. Ad absurdum we could say that the type checker could resolve the proper attribute x of all attributes named x based on the type of x but it makes no sense. In Java we can overload methods because we're missing named params and default values and we're lazy and want to name methods the same, but once we fix the first two issues we're left with lazyness. Nobody in their right mind would want to have multiple attributes with the same name, why are we lenient about methods having the same name?

@gavinking
Copy link
Member

A data point here is that on Saturday I spoke to some folks who seemed pretty bothered by the fact that we don't have overloading, and then when I demoed that I can write:

void foo(Float|Integer num) { ... }

and

void foo(Float num, Float opt = 0.0) { .... }

they were a lot happier. These are, after all, the two major usecases for overloading in Java.

@gavinking
Copy link
Member

Closing this issue, since the absence of overloading has become a distinctive feature of the language. Doesn't look like we're ever going to introduce it, except, perhaps, someday, in an impoverished form where it's basically a syntax sugar, i.e. some day we could support this:

class MyClass() {
    void print(String string) => ... ; 
    void print(Integer int) => ... ; 
    void print(Float float) => ... ; 
}

Since we could transform this code into:

class MyClass() {
    void print(StringIInteger|Float val) => 
        switch (val) case (is String) ... case (is Integer) ... case (is Float) .... ; 
}

But that would really be a different language feature, not "overloading" as such.

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

No branches or pull requests

3 participants