I’m trying to better learn and understand how to write idiomatic Julia. Please consider the following example. I have a some types defined as follows.
abstract type MyAbstractType end
Base.@kwdef struct MyStruct_1D <: MyAbstractType
# various fields that do not have a default value
dim::Int = 1
end
Base.@kwdef struct MyStruct_2D <: MyAbstractType
# various fields that do not have a default value
dim::Int = 2
end
These structs contain initial conditions, boundary conditions, physical parameters, and solutions on grids of either 1 or 2 dimensions, possibly 3 dimensions in the future. I want to define various methods that change behavior depending on the type. My naive approach then would be to do the following
function func1(s::MyStruct_1D)
# do 1D stuff
end
function func1(s::MyStruct_2D)
# do 2D stuff
end
Alternatively, I see traits talked about a lot. I don’t have much experience with them but I feel they are important and I should know how to implement them. I’m wondering if this is a good use case for them. I’m not certain it is, since the trait information is in the type name already (i.e., _1D). My naive first pass at implementing the above with traits would be to do something like this.
abstract type Dimension end
struct OneDimensional <: Dimension end
struct TwoDimensional <: Dimension end
Dimension(::Type{MyStruct_1D}) = OneDimensional()
Dimension(::Type{MyStruct_2D}) = TwoDimensional()
func1(s::T) where {T<:MyAbstractType} = func1(Dimension(T), s)
func1(::OneDimensional, s::T) where {T}
# do 1D stuff
end
func1(::TwoDimensional, s::T) where {T}
# do 2D stuff
end
I’m not sure which of the approaches is better. One last thing I thought about but I am not sure if its possible. Each struct has the dim field. Is it possible to somehow dispatch on that, using something like Val() to pass that information to the compiler?
It’s not clear what the goal is here, perhaps you should give a more complete example, but one thing that I’m pretty sure of is that there’s no point to storing a field for dimension count inside a type that’s only ever intended to support 1D (or 2D, in the case of the other type). That’s redundant.
Thank you! I think this is the crux of what I was after. I knew there was a way to embed the dimensionality somehow into the type itself in a more elegant way then _1D or _2D.
The prototype for encoding dimensionality into the type is AbstractArray{T,N} and Array{T,N}:
julia> const NArray{N} = Array{T,N} where T
Array{T, N} where {N, T}
julia> NArray{1}
Vector (alias for Array{T, 1} where T)
julia> NArray{2}
Matrix (alias for Array{T, 2} where T)
julia> NArray{3}
Array{T, 3} where T
julia> NArray{4}
Array{T, 4} where T
One advantage of the trait approach is that you can add your own dimension type to extend the method. For example
struct OneDFractal <: Dimension end
Dimension(::Type{MyStruct_Fractal}) = OneDFractal()
#dispatch
func1(::OneDFractal, s::T) where {T}
# do stuff
end
This is well explained in
Kwong, Tom. Hands-on Design Patterns and Best Practices with Julia: Proven Solutions to Common Problems in Software Design for Julia 1.x. Birmingham, UK: Packt Publishing, 2020.