Subtype or enum, which is prefer?

abstract type AbstractFruit end
struct Apple <: AbstractFruit end
struct Banana <: AbstractFruit end
struct Pear <: AbstractFruit end

color(::Apple) = :red
color(::Banana) = :yellow
color(::Pear) = :green

and

@enum Fruit apple banana pear

color(fruit::Fruit) = 
     fruit == apple ? :red :
     fruit == banana ? :yellow :
     fruit == pear ? :green :
     :unknown

makes the simular function. In intuition, I think the first one is better. But when it is warpped in a struct or Array, the first one will cause type Union, which will be compile into pointers and cause performance problems.

Consider

AbstractFruit[Apple(), Banana(), Pear()]  # which include 3 pointer
Fruit[apple, banana, pear]  # which include 3 `Int32`

In this condition, is the enum better than the subtype?

1 Like

It mostly depends on your needs: dynamism (e.g. adding new types in external packages) or performance.

Also, there are solutions to speed up the dispatch, if you really need the dynamism but wants to get some of the performance back.

For your example SingleDispatchArrays.jl seems the best. I maintain a list of similar packages in the docs of Catwalk.jl: Catwalk.jl Intro · Catwalk.jl

3 Likes

I’m wondering in which real-world use case multiple-dispatch would cause a significant performance problem.

To me, this is more of a design choice. If I’d like to take advantage of multiple-dispatch, subtyping is the only way. If the subtype is designed never to carry payloads in the future, I’d like to dispatch on the types instead of instances:

# dispatch on the types
abstract type AbstractFruit end
struct Apple <: AbstractFruit end
struct Banana <: AbstractFruit end
struct Pear <: AbstractFruit end

color(::Type{Apple}) = :red
color(::Type{Banana}) = :yellow
color(::Type{Pear}) = :green
# dispatch on the instances
abstract type AbstractFruit end
struct Apple <: AbstractFruit
    rotten::Bool  # extra info to determine color
end
color(x::Apple) = x.rotten ? :black : :red

What is the benefit of dispatching on types?

Personally, I use this style to indicate any subtypes should never take any payloads.

In the case above, the original version of color only took an instance of certain AbstractFruit types.
Then, I realized I need one more parameter, say rotten, to get the color of Apple. Without changing the interface of color, this new parameter(the payload) can be carried by the Apple type itself. This hack is not allowed if the color function is directly dispatched on the Type{Apple}.

1 Like

Looking at Base as an example, the dispatch is on ::Nothing and ::Missing not ::Type{Nothing} and ::Type{Missing}. Types also have various behavior such as not getting specialized on (Performance Tips · The Julia Language) etc. I would say using instances is in general preferable, even if you are not planning on bundling any data with it.

5 Likes

What if I don’t care about the performance. :upside_down_face:

For years, I got addicted to making every piece of my Julia code type-stable. Why? Probably because Julia is advertised by its performance, a good Julia user should write decent code keeping in mind what the Performance Tips say. But then, I realized it’s a bit overkill to optimize everything, especially for something that is not performance-sensitive. Now, I just want to be a clueless Julia user and write Julia code without thinking too much.

In intuition, I think the first one is better. But when it is warpped in a struct or Array , the first one will cause type Union, which will be compile into pointers and cause performance problems.

Now, if I hit something like this, I just follow intuition and I will follow my intuition on the dispatching-types- vs-dispatching-instances problem as well.

BTW, I did a check by running (@which color(...)).specializations, but the outputs were the same for the above examples. Am I missing something?

4 Likes

For years, I got addicted to making every piece of my Julia code type-stable. Why? Probably because Julia is advertised by its performance, a good Julia user should write decent code keeping in mind what the Performance Tips say.

Same here, only that I am in my second julian year, fully addicted. :slight_smile:

I also thought that dispatching on types vs instances is mostly question of style (vaguely remember reading that somewhere about traits), and I personally find instance-based code easier to read, which is the main reason of my style choices. OTOH encoding things in style is something I also like to do. Still, I do not see why it is important to forbid subtypes having members.

1 Like

In fact, I use instances in most of the case except certain really simple cases like the example in OP. :wink: