I’m using Configurations.jl quite extensively to run some simulations and it’s pretty amazing!
There is one scenario, however, which I’m not quite sure how to solve in an “elegant” way, so I just want to gather some thoughts from other people. Perhaps there is a completely different solution to my problem and I’m just not seeing it right now.
Problem
Suppose I have a core package which defines some parent option type MainOption
. One of its fields can be of one of different sub-option types, say SubOption1
and SubOption2
. This can be done straightforwardly by using a union of the subtypes similar to this
@option struct MainOption
sub::Union{SubOption1, SubOption2}
end
which automatically checks if the input fits one of the two concrete sub-option types.
Now consider a second package, which extends the core package and adds new features (which are not required or desired in the core package). These features get their corresponding sub-option types (say SubOption3
), but it’s not possible to use the MainOption
from the core package directly with the new sub-option, since the type union was hard-coded.
The basic question is: How to design an option type which is extensible from an outside package in this way (i.e. allow for adding new sub option types for a particular field)?
Obvious Solutions (?)
To my understanding, using abstract types or parametric types (which subtype some abstract type) for the fields are the only ways to allow the field type of a struct to be extended by types which are not known at the time of the struct definition.
But neither abstract, nor parametric types work out-of-the-box for option types, since the methods to construct the objects expect some concrete types.
Another obvious solution would be to just duplicate all the code in the extension package and make its own MainOptionExt
which contains all the concrete sub-option types known at that time. That introduces a lot of new code and maintenance burden though.
Current Approach
What I’m thinking about doing currently is this (there is also an example script below):
- use an abstract type to group the sub-options and make this the field type of the main option
- “dynamically” (i.e. at the time of construction, not definition) find all concrete subtypes of the abstract type when trying to construct a config object
- customize
from_dict
to get the behavior of the union types
This is how it looks like in a simple example:
Current solution
using Configurations
abstract type AbstractSubOption end
@option struct SubOption1 <: AbstractSubOption
key1::Int
end
@option struct SubOption2 <: AbstractSubOption
key2::Int
end
@option struct MainOption1
sub::Union{SubOption1, SubOption2}
end
@option struct MainOption2
sub::AbstractSubOption
end
d1 = Dict(
"sub" => Dict(
"key1" => 1
)
)
from_dict(MainOption1, d1) # Will work (goes through all the union types)
# from_dict(MainOption2, d1) # Won't work out of the box
#####
# Convenience function to grab all "leaf" types of an abstract type
using InteractiveUtils
function find_all_leaf_types!(leaveTypes, type)
for st in subtypes(type)
if isconcretetype(st)
push!(leaveTypes, st)
else
find_all_leaf_types!(leaveTypes, st)
end
end
return nothing
end
function find_all_leaf_types(type)
leaveTypes = Type[]
if isconcretetype(type)
push!(leaveTypes, type)
else
find_all_leaf_types!(leaveTypes, type)
end
return leaveTypes
end
#####
# With this, we can re-route the construction to
function Configurations.from_dict(MainType::Type{MainOption2}, of::Configurations.OptionField{:sub}, SubType::Type{AbstractSubOption}, x)
return Configurations.from_dict_union_type_dynamic(MainType, of, Union{find_all_leaf_types(SubType)...}, x)
end
from_dict(MainOption2, d1) # Will work now
@option struct SubOption3 <: AbstractSubOption
key3::Int
end
d3 = Dict(
"sub" => Dict(
"key3" => 1
)
)
# from_dict(MainOption1, d3) # Won't work, since the union was hard-coded in the option type
from_dict(MainOption2, d3) # Will still work, since the current type union is constructed
I’m wondering if this is really the easiest or most straightforward solution to my problem. And also if there are some fundamental downsides which I’m overlooking here (*). Perhaps my assumption about making structs accept new types are not correct?
I did see this post by the package author @Roger-luo , mentioning customization of the object type construction, but if I understood that correctly, it would come down to the same thing in the end. I would also need to hook in the function that finds the candidate concrete types from the given abstract types.
If there aren’t any general downsides, is there a chance that this could turn into a feature of Configurations.jl to be able to use abstract field types and keep option types extensible? I guess one argument against that would be the need to “crawl” all concrete subtypes, which could easily tank performance for really large abstract types…
(*) At this point I’m not worried about performance hits from having a config with abstract field type. The current approach could also be modified to use parametric types, but that makes the custom from_dict
method a bit more verbose.
As always, any hints, tips, comments are greatly appreciated