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?
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
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}.
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.
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?
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.
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.