Why traits?

I’ve been trying to teach my students traits, only to realize that there is something I don’t fully understand. Is there any different between these two approaches to dispatch?

struct A end
struct B end

## Holy trait

trait1(::A) = Val(true)
trait1(::B) = Val(false)
f(x) = f(x, trait1(x))
f(x, ::Val{true}) = π
f(x, ::Val{false}) = "pi"

## Type-inferrable if-else

trait2(::A) = true
trait2(::B) = false
g(x) = trait2(x) ? π : "pi"

Both make @code_warntype happy, and while I know the first one is more “julianic”, I actually wonder why.


The second one relies on compiler inlining and constant propagation (which might not happen if the functions become sufficiently complicated), whereas the first only relies on type inference and specialization (which happen even for very complicated functions).

(Technically, even in the first case you are relying on inlining and constant propagation because you are calling Val(true) rather than Val{true}(). But for the Val function with literal constants the two are equivalent these days; this wasn’t the case in early versions of Julia IIRC.)


Follow up: assuming I do call Val{true}() in trait1, is there any difference between using value-types and using dummy structs like TraitSatisfied()?

If you define your own types then you can have a hierarchy with subtypes. (There is also a difference in readability by using more descriptive names, of course.)


That makes sense!
First-class answers in a matter of seconds: 10/10 would recommend the Julia Discourse


With 2 maybe no, but if you have more you can apply the strategy pattern by modifying only one line (see design patterns and best behavior in Julia)

For example

abstract type Algo end
struct Fast:<Algo end
struct Slow:< Algo end
struct Experimental<:Algo end

Then you can dispatch multiple versions by simply doing different dispatch of


This is a great pattern if you want to maintain a growing code without awkwards if else switches inside