Should I use parametric types with parameters of type Symbol or not?

This may be a silly design question, but I had it often enough to feel like asking it here.
In the following example, would you recommend version 1, where name is a parameter or version 2, where name is a field? What are your considerations when deciding on such a design question?

julia> struct FoodItem{name}                                                                   
           weight::Float64                                                                     
           taste::String                                                                       
       end    
                                                                                 
julia>  name(::FoodItem{T}) where T = T
                                                                                
julia> @generated function isfruit(::FoodItem{name}) where name                                
           if name == :Apple || name == :Orange                                                
               return :(true)                                                                  
           else                                                                                
               return :(false)                                                                 
           end                                                                                 
       end                                                                                     
isfruit (generic function with 1 method)                                                       
                                                                                               
julia> struct FoodItem2                                                                        
           name::Symbol                                                                        
           weight::Float64                                                                     
           taste::String                                                                       
       end 

julia> name2(x::FoodItem2) = x.name                                                                                    
                                                                                               
julia> isfruit2(x::FoodItem2) = x.name == :Apple || x.name == :Orange                          
isfruit2 (generic function with 1 method)                                                      
                                                                                               
julia> x = FoodItem{:Apple}(.4, "sweet")                                                       
FoodItem{:Apple}(0.4, "sweet")                                                                 
                                                                                               
julia> x2 = FoodItem2(:Apple, .4, "sweet")                                                     
FoodItem2(:Apple, 0.4, "sweet")                                                                
                                                                                               
julia> using BenchmarkTools                                                                    
julia> @benchmark isfruit($x)                                                                  
BenchmarkTools.Trial:                                                                          
  memory estimate:  0 bytes                                                                    
  allocs estimate:  0                                                                          
  --------------
  minimum time:     0.018 ns (0.00% GC)
  median time:      0.022 ns (0.00% GC)
  mean time:        0.021 ns (0.00% GC)
  maximum time:     0.061 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

julia> @benchmark isfruit2($x2)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     1.963 ns (0.00% GC)
  median time:      1.976 ns (0.00% GC)
  mean time:        1.984 ns (0.00% GC)
  maximum time:     13.419 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

julia> @benchmark isfruit(x)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     12.185 ns (0.00% GC)
  median time:      12.294 ns (0.00% GC)
  mean time:        12.435 ns (0.00% GC)
  maximum time:     33.543 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     998

julia> @benchmark isfruit2(x2)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     3.576 ns (0.00% GC)
  median time:      3.588 ns (0.00% GC)
  mean time:        3.606 ns (0.00% GC)
  maximum time:     14.649 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

I prefer version 2. Unless there are strong reasons to include something in the type don’t do it. Just one simple argument; maybe you want to store Food in a container, like a vector. With version 2 you can have the vector concretely typed, whereas with version 1 you need it abstractly typed.

There are some sections in the manual about “value types” that you might find useful:
https://docs.julialang.org/en/v1/manual/performance-tips/#Types-with-values-as-parameters-1
https://docs.julialang.org/en/v1/manual/performance-tips/#The-dangers-of-abusing-multiple-dispatch-(aka,-more-on-types-with-values-as-parameters)-1

2 Likes

Thanks!