Inheritance or trait function?



I am facing a problem for which I am not 100% sure of the pros and cons of two different solutions. Imagine that I have an abstract type:

abstract type A end

and various concrete types:

struct T1 <: A end
struct T2 <: A end
# ...

Some of these types have a property, let’s say iscool(::T1) = true and iscool(::T2) = false. I wonder what are the pros and cons of specializing a method iscool for each type versus adding a new level of inheritance:

abstract type A end
abstract type Acool <: A end

struct T1 <: Acool end
struct T2 <: A end

At the caller site, I would be using either one of the following to determine the coolness of the object:

  • iscool(obj)
  • obj isa Acool

Could you please share your suggestions for choosing one solution over the other or any other better solution that you know in Julia?


What if you want to also add the spicyness of the object? With traits you just define another function. With inheritance you do what?


Very good point, this is definitely something to consider :+1:


Can we say that inheritance is the go-to solution only in cases where we can exploit the dispatch system? I understand that the iscool method will be checked in a if-else statement whereas the Acool type could be passed in as an argument to a function for a specialized implementation. So, if the if-else statement is called 1000000 times we have a performance issue, whereas the dispatch solution has no bottlenecks.


No, I went through this pretty extensively.

Try it yourself: the iscool(A::T1)=true will compile away in each of its usages (when A can be inferred as a T1, same as static vs dynamic dispatch). So the compiler uses this just fine and there’s no performance penalty for doing it like this. You can do this to build dispatch on traits which would be static dispatch actually.

It’s a style choice, not a performance choice.


No, you can use “Holy Traits” to have run time free traits

This is for example used for defining if abstract arrays support efficient linear indexing or not.


Defining type-level methods is my favorite approach.

iscool(::Type{<:A}) = false  # not cool by default
iscool(x::A) = iscool(typeof(x))  # value-level fallback method
iscool(::Type{T1}) = true  # but T1 is cool

This will also be optimized away and there is no runtime cost as long as it is type-stable. Type-level metaprogramming like generated functions works, too.