ceylon / ceylon-spec

DEPRECATED
Apache License 2.0
108 stars 34 forks source link

limited overloading #858

Open gavinking opened 10 years ago

gavinking commented 10 years ago

We could support a restricted form of function overloading where:

  1. all overloaded declarations have the same return type, and
  2. the overloaded declarations have disjoint parameter list types.

For this to be useful, we would be depending on #856.

Then, given an example like this:

Float f() => 0.0;
Float f(Float x) => x;
Float f(Integer x, Integer y) => x.float/y;

The function f would have the type:

Callable<Float,[]|[Float]|[Integer,Integer]>

which is actually the same type as:

Float() & Float(Float) & Float(Integer,Integer)

That is, I think, very natural. Notice a couple of things about this:

ncorai commented 10 years ago

You requested feedback: I don't really like the idea of having multiple ways to do the same thing, just as in [T*] and T[]. The translation work required when both occur in the same code (or code vs documentation) seems unwarranted by the benefit of catering to people who like either style. Very often you'll have cases where you need to pick one style for representation and it necessarily will clash in environments where the other style is favored.

Likewise, here one could define a function as several overloads and elsewhere refer to it as a single entity with type unions. What if one of the overloaded declarations itself has parameter type unions? Could one then make references to an overloaded version of a function that is not actually defined anywhere? I don't quite like where that leads.

Any chance I misunderstood and you only meant that we could override overloaded Java methods with a single Ceylon method having parameter type unions?

gavinking commented 10 years ago

I don't really like the idea of having multiple ways to do the same thing,

As a general principal, I completely agree. I don't want this to be a TMTOWTDI language. However, we reserve the right to break our own rules in special circumstances. A foolish consistency, &c ;-)

Likewise, here one could define a function as several overloads and elsewhere refer to it as a single entity with type unions.

To clarify, this proposal does add additional expressivity that doesn't exist today. Right now, there is no way to write the example above, even with defaulted parameters, even with union types. (Well, you can almost do it by writing a function that accepts a union of type types, and flatten()ing it, but I don't think people are actually going to want to do that.))

Yes, sure, in certain cases you would have the choice between using a union type and using overloading but honestly I don't think the two things compete that much. Certainly neither approach is a struct subset of the other.

What if one of the overloaded declarations itself has parameter type unions?

I don't see a problem here. Given rule 2, it's all well-defined.

Could one then make references to an overloaded version of a function that is not actually defined anywhere?

No. I don't understand what you mean.

gavinking commented 10 years ago

@ncorai Currently it's impossible to write a function with type Float() & Float(Float) & Float(Integer,Integer). You can write a function with type Float() & Float(Float) or Float() & Float(Integer,Integer) or Float() & Float(Float) & Float(Float,Integer), but not Float() & Float(Float) & Float(Integer,Integer).

gavinking commented 10 years ago

@ncorai If it helps, you can think of this not so much as overloading, but as being able to define a function for a "heterogeneous" domain (heterogeneous in the sense that it is the union of several different tuple types) by taking the union of several partial functions. That is, given the example above, we define a function:

f : 1 ∪ ℝ ∪ ℤ×ℤ → ℤ

simply by forming the union of the functions f₁ : 1 → ℤ, f₂ : ℝ → ℤ, f₃ : ℤ×ℤ → ℤ.

So it's all perfectly well-defined in terms of our type system. In Ceylon, our function domains are sets of tuples. Since we're perfectly able to represent heterogeneous sets of tuples, why shouldn't we be able to define functions on them?

ncorai commented 10 years ago

My question earlier was: would it be possible to write a single function f with type Float(Float) & Float(Float, Integer) and then pass around references to f with type Float(Float), as if referencing one of the two overloads that would be equivalent to the type defined above? If possible, it would be confusing to my mind, since no such function would have actually been defined.

Or, vice-versa, would it be possible to pass around a single f function with the complete type, even though overloaded Float f(Float x) and Float f(Float x, Integer) were actually declared?

gavinking commented 10 years ago

My question earlier was: would it be possible to write a single function f with type Float(Float) & Float(Float, Integer) and then pass around references to f with type Float(Float), as if referencing one of the two overloads that would be equivalent to the type defined above?

Yes, surely. A Float(Float) & Float(Float, Integer) is a Float(Float). And indeed, when you invoke it with a Float, only one of the overloaded versions get called. So it is, indeed, a reference to just one of the overloaded versions.

If possible, it would be confusing to my mind, since no such function would have actually been defined.

I don't see why this is confusing. To me it strikes me as quite elegant and useful!

gavinking commented 10 years ago

Or, vice-versa, would it be possible to pass around a single f function with the complete type, even though overloaded Float f(Float x) and Float f(Float x, Integer) were actually declared?

I don't understand this half of the question. Code example?

ncorai commented 10 years ago

Here's what I have in mind (hopefully, that's correct Ceylon)

interface Type {
    Float f(Float x) => x;
    Float f(Float x, Integer y) => x/y;
}
Float g(Float a, Integer b, Callable<Float, [Float]|[Float,Integer]> f) => f(a,b);
g(1.0, 2, Type.f);
gavinking commented 10 years ago

@ncorai it's almost correct, except that you can't call f without a receiving instance of Type. So let's rewrite your code like this:

Float f(Float x) => x;
Float f(Float x, Integer y) => x/y;
Float g(Float a, Integer b, Callable<Float, [Float]|[Float,Integer]> f) => f(a,b);
g(1.0, 2, f);

This would indeed be well-typed. But I don't see anything at all wrong with it! Indeed, you can already write the same thing today:

Float f(Float x, Integer y=1) => x/y;
Float g(Float a, Integer b, Callable<Float, [Float]|[Float,Integer]> f) => f(a,b);
g(1.0, 2, f);

since Callable<Float, [Float]|[Float,Integer]> is just the type Float(Float,Integer=).

gavinking commented 10 years ago

But I don't see anything at all wrong with it!

Well, OK, I mean, it's bad in the sense that the explicitly declared signature of g() is more restrictive than it needs to be. That is:

Float g(Float a, Integer b, Float(Float,Integer=) f)

Accepts fewer arguments than:

Float g(Float a, Integer b, Float(Float,Integer) f)

But I don't see what that's a problem. Ceylon's type system doesn't permit the type of f to be inferred, so the person defining g() has to explicitly write down the type of f. Why would they write the more complex type Float(Float,Integer=) instead of the simpler type Float(Float,Integer)? I can't see anyone making that mistake.

ncorai commented 10 years ago

My main problem is that it's not wysiwyg: in your IDE, you ctrl-click on f within g(1.0, 2, f), which overload does it bring you to, Float f(Float x) => x; or Float f(Float x, Integer y) => x/y;? I suppose having overloading is actually going to make the language easier to switch to for a lot of Java developers but, beyond that, it makes parsing it slightly harder (for my brain). It's all I wanted to say.

Don't forget to remove your reasons why overloading in Ceylon doesn't make sense (http://ceylon-lang.org/documentation/1.0/faq/language-design/#overloading).

gavinking commented 10 years ago

My main problem is that it's not wysiwyg: in your IDE, you ctrl-click on f within g(1.0, 2, f), which overload does it bring you to, Float f(Float x) => x; or Float f(Float x, Integer y) => x/y;?

Ah OK, now I understand the objection. Sure, that's a problem. Eclipse will let me pop up a list of the possible targets when I do a command-click, so you can choose which overloaded form to link to, but sure, that's still not perfect.

I suppose having overloading is actually going to make the language easier to switch to for a lot of Java developers

That, and more importantly it could make interop smoother. You'll be able to implement a Java interface with an overloaded method.

Don't forget to remove your reasons why overloading in Ceylon doesn't make sense

Here I wrote:

Well, overloading interacts with a number of other language features though, in truth, the interactions could probably be controlled by sufficiently restricting the signature of overloaded declarations.

Which is just what we're doing here. I'm formalizing the restrictions in a pretty reasonable and well-defined and intuitive way.

And overloading also maps badly to the JVM because generic types are erased from signatures. But there are potential workarounds for this problem, too.

This is indeed still a problem. Specifically, if we want to solve the interop problem, and the issue with generic types, the mapping to Java could turn out to be very messy.

method references to overloaded declarations are ambiguous.

The proposal above remove this ambiguity, by turning the overloaded functions into a single function.

RossTate commented 10 years ago

You will have to restrict this more carefully. Remember that in Ceylon, the types [Integer,Integer]|[Float,Float] and [Integer,Float]|[Float,Integer] are equivalent; in particular, they're both equivalent to [Integer|Float,Integer|Float]. At least, I think that's the case with the current incarnation of union types. Maybe we could look into redesigning union types, though.

gavinking commented 10 years ago

Remember that in Ceylon, the types [Integer,Integer]|[Float,Float] and [Integer,Float]|[Float,Integer] are equivalent; in particular, they're both equivalent to [Integer|Float,Integer|Float].

That's like totally not true ;-) They're definitely distinct types.

gavinking commented 10 years ago

FTR, [Integer,Integer]|[Float,Float] and [Integer,Float]|[Float,Integer] are both subtypes of [Integer|Float,Integer|Float], but they're definitely not equivalent to [Integer|Float,Integer|Float] nor to each other.

RossTate commented 10 years ago

Oops. I got unions and intersections mixed up. Too much type systems for me! Good thing I finally get a day off this week, heheh.

gavinking commented 9 years ago
  • Given the second restriction, the first restriction could probably be relaxed if we were to make the analysis of invocation expressions sufficiently clever. That is to say, the typechecker could unambiguously choose between the intersected function types by matching the argument tuple against the disjoint parameter tuple types.

Note that I've since discovered that this isn't correct: due to the principal instantiation inheritance restriction, and disjoint type analysis, this restriction can't be relaxed.

sirinath commented 9 years ago

Lack of overloading is an unnecessary restriction, this more philosophical than serving a practical purpose or making coding easier for the end user. I guess you should justify the language features by:

Every use case there overloading can be applied now needs a switch statement matching tuples. This can be complex if for new users coming from Java or other language but overloading is a natural concept in these language hence a flat learning curve. The pattern matching and specifying parameters start liking like ASCII art when variable length arguments, tuples, disjoin types and functions get into the mix. In overloading the 1st level of disjoint types and 2nd level of tuples can be omitted. This adds to clarity and also help towards a new programmer adopt or use known concepts it they are not familiar with ADTs.