Awkward case with multiple-dispatch and type heirachy

Some disclaimer: I am still learning Julia and I think it’s natural for me to compare it with a language I am fluent in – Rust. But I am not here arguing Rust is better than Julia or vice versa. I am totally happy and open to learning different paradigm.

I have been recently writing julia code and got into an awkward situation. I have a function f that has a fallback logic

function f(foo)
   # fallback logic
end

And I want to handle a special case for AbstractVector{<:Integer}. So I have

function f(foo::AbstractVector{<:Integer})
   # special case logic
end

However, the catch is Bool <: Integer, and so AbstractVector{Bool} <: AbstractVector{<:Integer}.

And because of the type invariance, I also cannot write AbstractVector{Integer} as AbstractVector{Int64} is not a subtype of it.

However, what I really want to do is to handle a special case for “vector of integer”. So to stop Bool from going into these special logic I have to duplicate the fallback logic for AbstractVector{Bool}. This is quite awkward to me.


So my questions are:

  1. What’s the best practice in this situation. Additionally, to avoid these situations, it seems like I have to know the type hierarchy very well. How to watch out for all these special cases?
  2. I understand true + 3 ==4 might be handy as are many cases detailed in the issue above. But I feel this case is not strong enough to make Bool always a subtype of Integer. In Rust, one can handle such situation by implementing an Add<Rhs = bool> trait for i32 for example. And this is essentially orthogonal to the rest of the code, i.e. in other cases, Bool and integer are quite independent from each other. My current understanding makes me feel Julia’s type hierarchy doesn’t have enough granularity that it should have.

There are probably more elegant solutions, but to address the specific problem of code duplication, you could define:

_f(foo) = # fallback logic
f(foo) = _f(foo)
f(foo::AbstractVector{Bool}) = _f(foo)
f(foo::AbstractVector{<:Integer}) = # My specialized case
2 Likes

The function subtypes may be helpful here :slight_smile:

1 Like

The type hierarchy in Julia is a tree. If you need some other division or want to step out of the hierarchy, you can use the Holy trait pattern:

struct Default end
struct Special end

f(foo) = _f(foo, traitof(foo))

traitof(x) = Default()
traitof(x::AbstractVector{T}) where {T<:Integer} = if T == Bool; Default() else Special() end

_f(foo, ::Default) = :fallback_logic
_f(foo, ::Special) = :special_logic

This actually quite similar to how types can be assigned to traits in Rust, but more flexible in that it readily works with multiple dispatch.

7 Likes

That’s not easy, since it’s possible to add new types to the hierarchy. Someone might e.g. define a ModularInteger{n} <: Integer and try to use your function. It may or may not work. If your function is only intended for “system” integer types, you can do somthing like this:

foo(x) = #fallback
const MyInteger = Union{Signed, Unsigned}
foo(x::AbstractVector{<:MyInteger}) = # the real code
2 Likes

You’re not alone, this was discussed pre-v1 and people have been occasionally making booleans less like integers ever since:
Introduce UInt1 type to replace misuse of Bool · Issue #18367 · JuliaLang/julia

The typical ways to handle this has been discussed already, and the only thing I can think to contribute is that if you don’t feel like moving logic to an internal helper method, you can use invoke for this purpose. It’s vulnerable to the fallback method being replaced by more specific methods though, so maybe that’s why it’s not used as much.

1 Like