Multiple inheritance is often discouraged in languages with concrete inheritance, like Python and C++. The situation in Julia is a little different.
Ambiguities certainly can be a problem, but I think they won’t be as much of a problem as one might think, which I will elaborate on below. First let me go over the examples again.
Benny’s example can be easily fixed. Since Package2
knows about interface B
, it can define an @idispatch
on B & D
, like this:
@idispatch f(x: B & D) = # ...
But we can come up with a more difficult situation, which is probably what Benny intended. I’ll describe that situation below, but first an aside.
This is not strictly needed for the discussion of ambiguities, but for the sake of completeness, I’ll mention that when designing a generic function f
, it should have a single, generic docstring. Further specializations of f
should not add new docstrings. A consequence of this is that the “hierarchy” of dispatches on f
(for a single argument) should have a single interface at the top, which is the interface mentioned in the docstring. Specializations of f
should dispatch on subinterfaces of that “top” interface.
So, here’s an example with a dispatch ambiguity that is challenging to resolve:
Single argument dispatch ambiguity
PackageA
function a end
@interface A begin
a
end
"""
foo(x: A)
Here is the generic definition of `foo`, which expects it's
argument `x` to implement the interface `A`.
"""
@idispatch foo(x: A) = 1
PackageB
using PackageA: foo, A
function b end
@interface B extends A begin
b
end
@idispatch foo(x: B) = 2
PackageC
using PackageA: foo, A
function c end
@interface C extends A begin
c
end
@idispatch foo(x: C) = 3
User code (or a fourth package)
using PackageA: foo
using PackageB: B
using PackageC: C
struct Mouse end
@type Mouse implements B, C
foo(Mouse())
The foo(Mouse())
call above throws an ambiguity error. The ambiguity error could be fixed by either PackageB or PackageC adding a package extension that defines a foo(x: B & C)
dispatch. That’s not an ideal solution, but it is tenable. However, I’m also going to argue in the next section that such ambiguities should be relatively uncommon. So, in the uncommon cases when an ambiguity does arise, it can be fixed with a new @idispatch
method in a package extension.
Ambiguities should be uncommon
There are two main categories of methods, and it’s useful to distinguish between them:
- Required methods:
- methods that must be implemented for an interface
- might have dozens of implementations
- usually dispatch on concrete types
- thus, they don’t have ambiguities
- Provided methods:
- generic functions defined in terms of required methods
- usually dispatch on abstract types (or interfaces)
- usually only a handful of specializations
- often, all implementations are defined by the function owner
- thus, ambiguities should be uncommon
One of the main reasons to implement an interface is to get all the provided methods for free. So if you create a custom type and you implement an interface for that type, you probably won’t add any specializations to the provided methods.
That’s not to say it won’t ever happen. If a library author creates an interface that extends an interface from another package, then they might also create additional specializations on their new interface for some provided methods.
So, to summarize, required methods shouldn’t have ambiguities, since they dispatch on concrete types. And ambiguities in provided methods will hopefully be uncommon, since there are typically only a small number of specializations, and those are often implemented by the function owner. And if absolutely necessary, ambiguities can be resolved in a package extension.
Here’s a concrete example that is inspired by Base Julia code, but is not actually Base code:
function iterate end
function length end
@interface Iterator begin
iterate
end
@interface SizedIterator extends Iterator begin
length
end
@idispatch mycollect(x: Iterator) = # ...
@idispatch mycollect(x: SizedIterator) = # ...
In this example:
iterate
and length
are required methods
- They will likely have hundreds of implementations for concrete types across the ecosystem.
mycollect
is a provided method
- The owner of the above code owns both implementations of
mycollect
.
- It’s likely that no other packages will define additional implementations of
mycollect
, in which case there will never be any ambiguities.