I have a package where I have a growing amount of types that contain each a specific type of data.
Each type follows a minimal interface described by :
A container number Cnum
A description Cdescription
Each type Container<Cnum> is defined in a separate <Cnum>.jl file.
I do not use parametric types as they do not seem to fit the purpose here (except for the number and description plus a few other things, different types have very different data contents).
My question is : How can I define functions for all of these types wihtout explicitely calling the declaration for each type ?
For instance is there a way to do something in the principle like below, i.e. read the available Cnum from a list of files and use this in a loop. Parametric types could be the solution because I totally may have missed something there.
# Helper function to extract the type number
read_ds_number_from_file(filename::String) = tryparse(Int64,first(splitext(filename)))
# Retrieve list of files related to each type
const supported_containers = read_cnum_from_files.(readdir(joinpath(@__DIR__,"containers")))
# Loop over container identifiers
for cn in supported_containers
# Get the actual type of a container
T = eval(Symbol("Container$filenum"))
# Declare functions based on this type
cnum(::S) where {S == T} = cn
description(::S) where {S == T} = eval(Symbol("description$filenum"))
description(::Val{filenum}) = description(T)
end
I have to say what I show here doesn’t feel right, and unsurprisingly fails…
Perhaps a bit more information would help to get replies.
I take it that you have a list of integer values cn (read from a file, but that seems incidental). For each cn you defined a struct (e.g., Container17 for cn == 17).
The goal is to programatically define methods of a function (say description) for each cn.
In your example, all the functions need to know about Container17 is the number 17. I’m assuming it’s not that simple? But if not, how would the method for Container17 be constructed from just knowing its name?
But assuming it is that simple, two obvious solutions are:
store the number 17 in the struct
make it a parametric type Container{T} (why not?)
It’s a bit hard to wrap my head around a use case where this pattern would be useful (i.e., defining a method based on the name of the struct while cn does not play any role other than naming structs in arbitrary order). I think explaining the objective might help.
Define a new abstract type and make all your containers subtypes. Use internal constructors to enforce the correct container number and container description. Then define other functions on the abstract type. Example:
abstract type MyAbstractContainer end
struct Container1 <: MyAbstractContainer
cn::Int
description::String
# Follow these with data specific to this concrete type
d1::Matrix{Float64} # for example
d2::Vector{Int} # etc.
Container1(d1, d2) = new(1, "I am a Container1", d1, d2)
end
struct Container2 <: MyAbstractContainer
cn::Int
description::String
# Follow these with data specific to this concrete type
f1::Vector{Float64} # for example
f2::Int # etc.
Container2(f1, f2) = new(2, "I am a Container2", f1, f2)
end
cnum(x::MyAbstractContainer) = x.cn
description(x::MyAbstractContainer) = x.description
With these definitions in place, you can do the following:
julia> c2 = Container2([1.,3.], 4)
Container2(2, "I am a Container2", [1.0, 3.0], 4)
julia> description(c2)
"I am a Container2"
julia> cnum(c2)
2
Yes, the important part is the number, because this number totally defines the data structure of interest. For instance, in file 17.jl I have a const description17 = "Some description 17" because the description is specific to the type, not to each instance of the type, a bit like what would be in a docstring. The same goes for the number, I don’t want to store this kind of info in each instance, which may be numerous.
This is probably a good way to do it, but I’m not sure I can do e.g. description(::Container{T}) where {T == 17} = "Some description 17" can I ?
Thanks for the proposal, but it does not fit my purpose. The goal is not to attach the info in each instance, but to the type itself. That would result in a lot of data duplication.
julia> struct Container{T} end
julia> description(::Container{T}) where T = "Some description $T"
description (generic function with 1 method)
julia> description(Container{17}())
"Some description 17"
But you can do it more flexibly in a way similar to what you tried in your original post if you get the syntax right:
julia> spec = Dict(1 => "description of the first kind of container",
2 => "second kind of container")
Dict{Int64, String} with 2 entries:
2 => "second kind of container"
1 => "description of the first kind of container"
julia> for (i, desc) in spec
@eval description(::Container{$i}) = $desc
end
julia> description(Container{1}())
"description of the first kind of container"
julia> description(Container{2}())
"second kind of container"
julia> macro add_description(num)
T = esc(Symbol("Container$num"))
f = esc(:description)
desc_str = Symbol("description$num")
quote
struct $T end
$f(::Type{$T}) = $desc_str
$f(::Val{$num}) = $desc_str
end
end
@add_description (macro with 1 method)
julia> const description8 = "Eight is the best" "Eight is the best"
julia> const description42 = "Answer to the Ultimate Question of Life, the Universe, and Everything"
"Answer to the Ultimate Question of Life, the Universe, and Everything"
julia> @add_description(8)
description (generic function with 2 methods)
julia> @add_description(42)
description (generic function with 4 methods)
julia> description(Container8)
"Eight is the best"
julia> description(Container42)
"Answer to the Ultimate Question of Life, the Universe, and Everything"
julia> description(Val(8))
"Eight is the best"
julia> description(Val(42))
"Answer to the Ultimate Question of Life, the Universe, and Everything"
This is pretty much what my intent was, but I’ve never used julia macros before. Thanks for this example !
I just have a comment : what is the purpose of the struct $T end ?
Yes, usually that would be enough, here the description is used as an example, but this is an oversimplification of my program. In the end, the macro only writes the code at your place…