Declaration
Declaring an overloaded function is pretty similar to C# — no two functions can exist with the same signature. Function signature consists of the function name and a list of parameter type “signatures”. That’s it.
There are only three nit-picking issues — type signature is well, type (Int
, String
) plus the number (depth) of pointer/reference access. For example a pointer to a pointer of a String
makes the same signature as a reference to a pointer of a String
(type is String
, depth in both cases is 2). On the other hand a pointer to String
does not have the same signature as String
. The reason for treating reference the same as pointer is none — simply I don’t have experience with language with such feature (not-null pointer) thus I prefer to play it safe.
The second constraint — if parameter types on given position are related, the names have to be the same.
And the last condition — overloading has to be more than just shuffling the parameter names.
Examples:
def foo(x Int, y String) Void = ... def foo(x Int, p Point) Void = ...
That’s OK, second parameter has inconsistent naming but neither String
is an ancestor of Point nor vice versa.
def foo(x Int, y String) Void = ... def foo(z IObject, p Point) Void = ...
That’s incorrect. The first parameter has related type (Int
is inherited from IObject
) so it has to have the same name.
def foo(x Int, y String) Void = ... def foo(y String, x Int) Void = ...
This is invalid code as well, second form brings nothing else than shuffled parameter names. To fix it you would have to rename one of parameters, or add an extra parameter (or drop overloading of course).
This rule may look strange, however I can only hope it is enough for preventing “too much information” surprise… Imagine this rule does not exist, and you have a call of the function “foo
”:
foo(5, 'hello');
The compiler resolves this nicely calling first version of “foo
”. But then you decide to be 100% accurate, so you add names for parameters:
foo(x : 5, y : 'hello');
In such case being precise would work against you (surprise) — it is impossible for compiler to resolve this call, because there are two functions perfectly matching the call. Shuffling names just bit you.
Resolving the call
During compile time the function candidates are selected in four steps:
- all functions with matching name and number of parameters are selected
- the number of functions is narrowed down by selecting only those which match parameter names
- further filtering — by checking parameter type (formal parameter has to be equal to given argument or has to be ancestor of argument)
- the one which is closest to formal parameter types is selected for call
The last step is mysterious a bit so let me explain this step in details — each possible version of overloaded function is processed through the list of type match values. If argument and parameter have exactly the same type we have match — it gives value 0. Otherwise (parameter type is an ancestor of the argument type) it gives value 1. For a function with three parameters we could end up with a list (1,0,1)
.
Then compiler picks the list which has consistently smaller values than the other lists — i.e. all values are not greater than in compared function (at the same position) and at least one value is strictly less than in compared function (at the same position).
Yes, example! Assume String
inherits directly from IObject
, and RichString
inherits from String
. Having overloaded function:
def foo(x RichString, y String, z IObject) Void = ... def foo(x IObject, y IObject, z String) Void = ...
and a call:
foo(new RichString('hello'), 'world', '!');
we can calculate match values for the types. For the first version of the function “foo
” they are (0,0,1)
and for the second — (1,1,0)
.
The call is ambiguous, because none of the versions gives consistently smaller list. Let’s compare those numbers one by one:
0 < 1 0 < 1 1 > 0
Because once one list has smaller value, and another time the other list has smaller value we cannot say that one list is consistently smaller than the other.
Note that it does not matter how many values are smaller on the list — position is crucial. In other words you cannot just count zeros and say the first version is closest because it has 2 zeros against 1 zero in second version. Also it does not matter that we have more “specialized” call in case of picking up the first version (RichString
).
Those posts are not only a documentation what I’ve done so far, it is also a good check-point to verify presented solution. At first I implemented “closest types” as from left to right comparison, but when writing example I realized it is easy to implement and describe, but it is also a poor choice (the first parameter is no better than the last one) — thus I did it the way big players do. Thank you C# team.