Subtyping Dict

I’m using Dicts to store the parameters store the parameters of a problem to be solved by an algorithm. There are different types of problems and I would like to use multiple dispatch to organize the code for running the algorithm and plotting the results.

Right now I have something like

function run_problem1(params)
    Do something generic
    Do something specific to problem 1
end
function run_problem2(params)
    Do something generic
    Do something specific to problem 2
end
function plot_problem1(results)
    Do something specific to problem 1
end
function plot_problem2(results)
    Do something specific to problem 1
end

The duplication of code isn’t great and I am looking for a way to minimize it.
Ideally I would like to have something that looks like

function run(params::problem_type)
    Do specific thing
end
function start_run(params)
    Do generic thing
    run(params)
end
function plot(params::problem_type)
    do specific thing
end

However I don’t want to give up all the sweet properties of Dicts, so I don’t want to rewrite everything to use structs.
I realize I could define a struct and then define all the Dict methods for that struct but that seems like the kind of reinventing the wheel people always advise against.
So is there a way I create a Dict with a type? I.e. Problem1Params<:Dict, which will allow me to use multiple dispatch while still using Dicts?

This thread might be relevant to your problem:

In particular, the @forward macro is what I would use in this situation.

1 Like

Which properties of Dicts are important to you?

I was dealing with a very similar issue and ended up with following: my run functions take 2 arguments, special solver struct, e.g. Solver1Params or Solver2Params that contain the parameters for specific algorithms and that are dispatched on. I feel like the prior definition of all parameters in the struct gives me better control and readability.
For parameters that are independent of the specific algorithm and are more problem specific, I use dictionaries. To build them, I use the @dict macro from DrWatson.jl (which by the way is really useful package) that builds dictionary from variables.

Anyway, to do what you are asking, according to the docs, I believe you would have to subclass AbstractDict and implement parts of the interface that you require. The doc seems to be here, but I couldn’t find the interface documented in a similar ways as it is for iteration. However, looking at the sources for Dicts, it looks to me like it could be a lot of work with potentially mixed results (you would have to do write the interface for each of the concrete types, bar some packages that simulate subclassing and automate that for you).

Overall, from my understanding, encapsulating the dicts in struct could be less work and easier to maintain than subtyping AbstractDict, but I am curious to know what you come up with. I think a lot depends on what Dict properties are important to you.

Would this be a satisfactory workaround?

abstract type AbstractProblem end
struct Problem1 <: AbstractProblem end
struct Problem2 <: AbstractProblem end


function run(::Type{Problem1}, params)
    Do specific thing
end
function run(::Type{Problem2}, params)
    Do specific thing
end

function start_run(::Type{P}, params) where {P <: AbstractProblem}
    Do generic thing
    run(P, params)
end

function plot(::Type{Problem1}, params)
    Do specific thing
end
function plot(::Type{Problem2}, params)
    Do specific thing
end

So you can run it as

julia> start_run(Problem1, params)

etc.

2 Likes

Thanks for the replies.
Though I am still interested in how this could be done, I am going to try @heliosdrm’s suggestion as it seems much easier to work with than actually combining a new type with Dict.

Edit: It seems to work pretty well so far with

params = Dict(:problem_type => :Problem1, ...)
function run(::Val{:Problem1}, params) ... end
function start_run(params)
    ...
    run(Val(params[:problem_type]), params)
end

You found yourself a solution, good job!

Just in case, I would point out that if performance is relevant for your problem, with that solution you are adding some type-instabilities that might slow down your code. Nevertheless, if that’s not a pain point in your use case and the solution works, you can just go ahead and focus on more relevant parts of your problem.

Thanks, it’s basically your solution without the separate struct definition.

Is the type instability down to using ::Val instead of a predefined struct?

There are two possible sources of type instability added by this solution:

One is, as you guess, your using Val. I understand that this may be a bit confusing at first sight: Val(:Problem1) is of a concrete type, but Val(params[:problem_type]), as found in start_run, is not concrete, because your function does not know at compile time what will be the content of the thing inside the parentheses (i.e. the parameter of the Val type). So, that particular function won’t be properly optimized. If the rest of the code in that function is performance-sensitive, you may want to use a “function barrier”. See the entries about value types and function barriers in the Julia manual.

The other (potential) source is that if the types of keys/values of params are other than Symbol for the rest of entries, adding :problem_type => :Problem1 will force you to use a dictionary with abstract types. For instance, maybe the dictionary without :problem_type was something like Dict(:a=>1, :b=>1.5), i.e. a Dict{Symbol, Float64}. But with the new item it will be a Dict{Symbol, Any}. This is also completely legal, but functions that have to get values from that dictionary won’t be able to infer what type of value should be expected. (This being said, if params already combined different types of values, adding that new item will make no difference.)

To make it clearer: as by your definition

function start_run(params)
    ...
    run(Val(params[:problem_type]), params)
end

If the code abstracted by the “dots” is complex (and type stable) it’s recommended to wrap it into a different function, so that it can be compiled properly, and not spoiled by the type instability of the following line.

1 Like