Generate type specific code when type is a variable in a loop

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, you summed it up very well.

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.

This works though:

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"

Maybe this will give you some inspiration.

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"
1 Like

Sorry, I have misled you with this example, the description can be very different between containers.

Your second example is getting very close !

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 ?

The part that I’m not seeing is: Usually one would just do

struct Container17
  [fields]
end

description(::Container17) = "Description for seventeen"

Isn’t that the easiest approach? How is

const description17 = "Description seventeen"

plus a macro easier?

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…

This is not really something I would use a macro for but if it works for you, fine.

However, be aware that it relies on being called with literals. Doing

for i in (8, 42)
    @add_description(i)
end

is not at all the same thing as

@add_description(8)
@add_description(42)

This is one way to see the difference:

julia> @macroexpand @add_description(8)
quote
    #= REPL[19]:6 =#
    struct Container8
        #= REPL[19]:6 =#
    end
    #= REPL[19]:7 =#
    description(::Main.Type{Container8}) = begin
            #= REPL[19]:7 =#
            Main.description8
        end
    #= REPL[19]:8 =#
    description(::Main.Val{8}) = begin
            #= REPL[19]:8 =#
            Main.description8
        end
end

julia> i = 8; @macroexpand @add_description(i)
quote
    #= REPL[19]:6 =#
    struct Containeri
        #= REPL[19]:6 =#
    end
    #= REPL[19]:7 =#
    description(::Main.Type{Containeri}) = begin
            #= REPL[19]:7 =#
            Main.descriptioni
        end
    #= REPL[19]:8 =#
    description(::Main.Val{Main.i}) = begin
            #= REPL[19]:8 =#
            Main.descriptioni
        end
end
1 Like

This is indeed a difference, I noticed it too, but your warning and example are very much welcome.