Julian way to implement structs with different "defaults"?

Which could be a “julian” way to implement multiple struct defaults?

Like:

abstract type Parameters end

Base.@kwdef struct ParametersDefault1 <: Parameters
    x = 5
    y = [1,2,3]
    w = 10
    z = x .+ y 
end

Base.@kwdef struct ParametersDefault2 <: Parameters
    x = 5
    y = [10,20,30]
    w = 10
    z = x .+ (2 .* y) 
end

a = ParametersDefault1(x=1)
b = ParametersDefault2(x=1)

Where the names and types of the fields would be the same between the various “defaults”.
In my case, performances are not much of a concern, as most of the time is then spent in Ipopt.

Perhaps just using different external constructors, altought I still need to explicitly retype the unchanged parameters that are part of other computed defaults:

Base.@kwdef struct Par
    x = 5
    y = [1,2,3]
    w = 10
    z = x .+ y 
end
function ParDefault1(;kwargs...)
    p = Par(;kwargs...) 
    return p
end
function ParDefault2(;
    x = 5,
    y = [10,20,30],
    z = x .+ (2 .* y) ,
    kwargs...)
    p = Par(;x=x,y=y,z=z,kwargs...) 
    return p
end
c = ParDefault1(x=1)
d = ParDefault2(x=1)

What I think is not very “julian” there is the fact that ParDefaults1 (or 2) do not return an instance of the ParDefaults1 type. In this sense the first option is more appropriate.

On the other side, another option is to use a function and types only for dispatch, like:

struct ParDefaults1 end 
struct ParDefaults2 end

function set_parameters(::Type{ParDefaults1}; kargs...)
     return Par(...)
end function

function set_parameters(::Type{ParDefaults2}; kargs...) 
    return Par(...)
end

pars = set_parameters(ParDefaults1; x=1)

Thank you. Your solution aside the “same name / dispatch on type” is very similar to my first one.
I am however left with the fact I still need to type p = Par(;x=x,y=y,z=z,kwargs...) where in my real case x, y and z are hundreds (and so are the parameters that don’t change, the “w” in the example).. is there a way I can avoid to type them in the Par constructor ?

I there is where the @kwdef enters, as you are using it. I’m not sure if I get if there’s another problem there.

If x,y,z... are hundreds I would guess they are probably better grouped in a vector, I imagine.

The whole idea is to have a struct where I have the same fields but different “defaults” where then users can change something punctual (a few parameters) relative to these “defaults”.

I am fine with your solution, I just need to find now a way to “automatically” get the keyword arguments of a function.

For example, in:

function foo(;
    x = 5,
    y = [10,20,30],
    z = x .+ (2 .* y) ,
    kwargs...)
    p = printme(;x=x,y=y,z=z,kwargs...) 
    return p
end
function printme(;kwargs...)
    for (k,v) in kwargs
        println("$k: $v")
    end
end
foo(;x=10, a=1,b="a")

How to avoid typing “x=x,y=y,z=z” ? Is there a macro that provides the keyword arguments (used or not) of a function ?

I can see this old thread, but here I need it inside the function itself, so using that method would lead likely to infinite recursion…

I’ve done something like this before:

julia> function set_args(;args...)
           def_args = Dict(
               :x => 1,
               :y => 2,
           )
           for (key, val) in args
               if haskey(def_args, key)
                   def_args[key] = val
               else
                   error("arg $key no found")
               end
           end
           return def_args
       end
       _foo(args...) = println(args...) # internal
       foo(;kargs...) = _foo(set_args(;kargs...)...) # API
foo (generic function with 3 methods)

julia> foo(;x=10)
:y => 2:x => 10

julia> foo(;z=10)
ERROR: arg z no found

Thank you again. Yes, I saw the idea to using a Dict, but in my case(as in the example posted) I have some computed args that depend on the other args (the user can redefine the raw args or the way the computed ones, the one used in the model, are made), and basic dicts don’t work in this case.

How about the following? Just use one struct and use two possible functions to construct it, each of which have default values for all arguments?

abstract type Parameters end

struct ParameterType{X,Y,W,Z} <: Parameters
    x::X
    y::Y
    w::W
    z::Z
end

ParametersDefault1(; x = 5, y = [1,2,3], w = 10, z = x .+ y) = ParameterType(x, y, w, z)
ParametersDefault2(; x = 5, y = [10,20,30], w = 10, z = x .+ (2 .* y)) = ParameterType(x, y, w, z)

a = ParametersDefault1(x=1)
b = ParametersDefault2(x=1)
display(a)
display(b)

Keyword arguments is just a named tuple, you can splat it i.e.

kwargs=(k1=v1,k2=v2,k3=v3)
f(;kwargs...)

Also not quite what you want, but also have a look at

FWIW, you can type simply foo(;x, y, z, kargs...) to propagate the kargs x,y,z of the input. Appart from that I do not see how to simplify further from what @kwdef gives you.

1 Like

What about providing the two sets of default kwargs as namedtuples for the user to refer to?
Definition:

@kwdef struct MyParameters
...
end

MyDefaults1 = (x=5, y=...)
MyDefaults2 = (x=10, y=...)

Usage:

MyParameters(; MyDefaults1..., x=1)
MyParameters(; MyDefaults2..., x=1)

Seems the most straightforward imo.

4 Likes

Nice, I didn’t know that Julia would not complain about “repeated” keyword argument. I still however have the issue of loosing the possibility to compute argument values on the fly based on other arguments, i.e.:

@kwdef struct MyParameters
    x = 1
    y = 2
    z = x+y
end
    
MyDefaults1 = (x=10, y=20)
MyDefaults2 = (x=1,y=200,z=x+2*y) # This doesn't work

MyParameters(; MyDefaults1...,x=1000,z=3000) # Here Julia doesn't complain about double x keyword and takes 1000

I’m not sure I’m proud of this variation but it might satisfy your requirements.

@kwdef struct MyParameters
    x = 1
    y = 2
    z = x+y
end

my_defaults1(; x=10, y=20) = Base.@locals()
my_defaults2(; x=1, y=200, z=x+2*y) = Base.@locals()

Now you can do

julia> MyParameters(; my_defaults1()...)
MyParameters(10, 20, 30)

julia> MyParameters(; my_defaults1()..., x=1000, z=3000)
MyParameters(1000, 20, 3000)

julia> MyParameters(; my_defaults2(x=1)...)
MyParameters(1, 200, 401)
2 Likes

Thanks, I am going to test it…
This works, at the cost to use a small external package for the Struct to NamedTouple convertion…

using NamedTupleTools # for the ntfromstruct() function
@kwdef struct MyParameters
    x = 1
    y = 2
    w = 3
    z = x+y
    o = x*y
end

@kwdef struct MyDefaults1
    x = 10
    y = 20
end

@kwdef struct MyDefaults2
    x=1
    y=200
    z=x+2*y
end

function mydefaults1(;kwargs...)
  return MyParameters(; ntfromstruct(MyDefaults1())...,kwargs...) 
end
function mydefaults2(;kwargs...)
    return MyParameters(; ntfromstruct(MyDefaults2())...,kwargs...) # Here Julia doesn't complain about double x keyword and takes 1000
end

mydefaults1(x=1000,z=3000)      # (1000,20,3,3000,20000)
x=1; y=3000; o=x+y;
mydefaults2(;x=1,y=3000, o=x+y) # (1,3000,3,401,3001)

If you have variables in the default that depend on other stuff,
why not just have a function at that point?

@kwdef struct MyParameters
    x = 1
    y = 2
    z = x+y
end
    
MyDefaults1 = (x=10, y=20)
MyDefaults2(; x = 1, y = 200) = (x=x, y=y, z=x+2*y)
# you can even chain defaults:
MyDefaults3(; kwargs...) = MyDefaults2(kwargs..., x = 2)

EDIT: This is the poor man’s version of Gunnar’s suggestion :smiley:

1 Like

Yes, this works:

@kwdef struct MyParameters
    x = 1
    y = 2
    w = 3
    z = x+y
    o = x*y
end

my_defaults1(; x=10, y=20)          = Base.@locals()
my_defaults2(; x=1, y=200, z=x+2*y) = Base.@locals()

function mydefaults1(;kwargs...)
  return MyParameters(; my_defaults1()...,kwargs...) 
end
function mydefaults2(;kwargs...)
    return MyParameters(; my_defaults2()...,kwargs...) # Here Julia doesn't complain about double x keyword and takes 1000
end

mydefaults1(x=1000,z=3000)      # (1000,20,3,3000,20000)
x=1; y=3000; o=x+y;
mydefaults2(;x=1,y=3000, o=x+y) # (1,3000,3,401,3001)

Thank you for letting me discover Base.@locals(). Your solution creates a dictionary of local variables that are then iterated with the splat operator… Thank you!

FYI: the more “standard” and popular version of this is ConstructionBase.jl 's getproperties().

1 Like

FieldMetadata.jl lets you add multiple columns of arbitrary named metadata to structs, Which you can use ConstructionBase.setproperties to update.