class A; end
class B < A; end
class C; end
class D < C; end
def f(x1 : B, x2 : C); 1 end
def f(x1 : A, x2 : D); 2 end
puts f(B.new, D.new) #=> 1
This code will call the first function defined and print 1. If you change the order of two function definitions, it print 2 instead.
But for C++, same code cause an error: error: call of overloaded ‘fun(B&, D&)’ is ambiguous
#include <iostream>
class A {};
class B : public A {};
class C {};
class D : public C {};
int fun(A x1, D x2) {return 1;}
int fun(B x1, C x2) {return 2;}
int main() {
B b;
D d;
printf("%d", fun(b, d));
}
The way Crystal works, it tries to sort overloads if possible, otherwise the first overload that matches the call argument is picked up. This is not by design, but it’s how things work now.
I think we never bothered handling this case because it’s pretty uncommon. Is there a real use case for this?
It’s interesting to see C++ handles this case well. I guess we can do the same thing for Crystal. But how do you disambiguate the call in C++?
And there is no perfect one-fits-all solution for solving ambiguities: either you restrict valid cases, or you use a predictable heuristic (like “pick the first one”). Part of Crystal’s power comes from its flexibility, which is undoubtedly a two-sided sword: subtlety changing the type of an object may silently change the overloading that is picked, and the behavior of the program. If, on the other hand, we decide to have a stricter language, then the compiler will annoy you when there’s no real reason for it.
I’m not saying Crystal’s approach is right here, I’m just pointing out that the cost of such checks aren’t free. And many times they even won’t remove entirely the ambiguity. Many compilers attempt to be perfectly unambiguous but are nevertheless easy to fool.