Pass arguments to struct constructor

Hello,

I am working on code for an economic model. The solutions to the model are stored in arrays within an immutable struct. The dimensions of those arrays are drawn from another struct that holds hyperparameters for the numerical solution method (these govern the precision, domain, etc., of the solution).

So a highly simplified example of my solutions currently looks like:

@with_kw struct HHsol
    aP::Array{Float64,2} = fill(1.0,HYP.na, HYP.nz)
    cP::Array{Float64,2} = fill(1.0,HYP.na, HYP.nz)
end

where HYP is an instance of the struct containing the hyperparameters.

I’m doing simulations where I need to create a new instance of the solution structure but with different dimensions for the arrays drawn from a different instance of the hyperpameters structure. Is there a way that I can pass in the name of the hyperpameters structure to the solution constructor?

…something like:

@with_kw struct HHsol(HYPname)
    aP::Array{Float64,2} = fill(1.0,HYPname.na, HYPname.nz)
    cP::Array{Float64,2} = fill(1.0,HYPname.na, HYPname.nz)
end

The real solutions structure has many elements so that It would be cumbersome to have to directly specify the values rather than relying on default values when calling the constructor.

Thanks in advance for any advice!

You basically want to create your own macro here.

See Metaprogramming · The Julia Language

@with_kw @fill_params HYPname struct HHsol
    aP::Array{Float64,2}
    cP::Array{Float64,2}
end

To start let’s create an expression:

julia> e =:(
       struct HHsol
           aP::Array{Float64,2}
           cP::Array{Float64,2}
       end)
:(struct HHsol
      #= REPL[14]:3 =#
      aP::Array{Float64, 2}
      #= REPL[14]:4 =#
      cP::Array{Float64, 2}
  end)

julia> typeof(e)
Expr

julia> e.head
:struct

julia> e.args
3-element Vector{Any}:
 false
      :HHsol
      quote
    #= REPL[14]:3 =#
    aP::Array{Float64, 2}
    #= REPL[14]:4 =#
    cP::Array{Float64, 2}
end

julia> e.args[2]
:HHsol

julia> e.args[3]
quote
    #= REPL[14]:3 =#
    aP::Array{Float64, 2}
    #= REPL[14]:4 =#
    cP::Array{Float64, 2}
end

We see that we primarily want to edit argument 3 which is a quote block.

julia> e2 = e.args[3]
quote
    #= REPL[14]:3 =#
    aP::Array{Float64, 2}
    #= REPL[14]:4 =#
    cP::Array{Float64, 2}
end

julia> typeof(e2)
Expr

julia> e2.head
:block

julia> e2.args
4-element Vector{Any}:
 :(#= REPL[14]:3 =#)
 :(aP::Array{Float64, 2})
 :(#= REPL[14]:4 =#)
 :(cP::Array{Float64, 2})

julia> e2.args[3]
:(#= REPL[14]:4 =#)

julia> e2.args[2]
:(aP::Array{Float64, 2})

julia> e2.args[2].head
:(::)

julia> e2.args[2].args
2-element Vector{Any}:
 :aP
 :(Array{Float64, 2})

Now we need to figure how we want to modify this expression. To do so we create the expression that we want and examine it first.

julia> target = :(aP::Array{Float64, 2} = fill(1.0,HYPname.na, HYPname.nz))
:(aP::Array{Float64, 2} = fill(1.0, HYPname.na, HYPname.nz))

julia> target.head
:(=)

julia> target.args
2-element Vector{Any}:
 :(aP::Array{Float64, 2})
 :(fill(1.0, HYPname.na, HYPname.nz))

We see that all we want to do is replace the :: expression with a :(=) expression containing it. Let’s write a function to do that.

julia> function fill_individual_param(param_expr, s)
           Expr(
               :(=),
               param_expr,
               :(fill(1.0, $s.na, $s.nz))
           )
       end
fill_individual_param (generic function with 1 method)

julia> fill_individual_param(e2.args[2], :HYPname)
:(aP::Array{Float64, 2} = fill(1.0, ($(Expr(:escape, :HYPname))).na, ($(Expr(:escape, :HYPname))).nz))

julia> e2.args
4-element Vector{Any}:
 :(#= REPL[13]:3 =#)
 :(aP::Array{Float64, 2})
 :(#= REPL[13]:4 =#)
 :(cP::Array{Float64, 2})

julia> [isa(param, Expr) ? fill_individual_param(param, :HYPname) : param for param in e2.args]
4-element Vector{Any}:
 :(#= REPL[13]:3 =#)
 :(aP::Array{Float64, 2} = fill(1.0, ($(Expr(:escape, :HYPname))).na, ($(Expr(:escape, :HYPname))).nz))
 :(#= REPL[13]:4 =#)
 :(cP::Array{Float64, 2} = fill(1.0, ($(Expr(:escape, :HYPname))).na, ($(Expr(:escape, :HYPname))).nz))

Now we can use this replace the args of the quote block.

julia> e.args[3].args =  [isa(param, Expr) ? fill_individual_param(param, :HYPname) : param for param in e2.args]
4-element Vector{Any}:
 :(#= REPL[13]:3 =#)
 :(aP::Array{Float64, 2} = fill(1.0, ($(Expr(:escape, :HYPname))).na, ($(Expr(:escape, :HYPname))).nz))
 :(#= REPL[13]:4 =#)
 :(cP::Array{Float64, 2} = fill(1.0, ($(Expr(:escape, :HYPname))).na, ($(Expr(:escape, :HYPname))).nz))

julia> e
:(struct HHsol
      #= REPL[13]:3 =#
      aP::Array{Float64, 2} = fill(1.0, ($(Expr(:escape, :HYPname))).na, ($(Expr(:escape, :HYPname))).nz)
      #= REPL[13]:4 =#
      cP::Array{Float64, 2} = fill(1.0, ($(Expr(:escape, :HYPname))).na, ($(Expr(:escape, :HYPname))).nz)
  end)

We can then turn that into a function:

julia> function fill_params(e, s, m)
            e.args[3].args =  [isa(param, Expr) ? fill_individual_param(param, s) : param for param in e.args[3].args]
            return Parameters.with_kw(e, m, true)
       end
fill_params (generic function with 1 method)

julia> fill_params(
           :(struct HHsol
                  aP::Array{Float64,2}
                  cP::Array{Float64,2}
           end),
           :HYPname,
           @__MODULE__
       )
quote
Base.@__doc__ struct HHsol
            "Default: fill(1.0, HYPname.na, HYPname.nz)"
            aP::Array{Float64, 2}
            "Default: fill(1.0, HYPname.na, HYPname.nz)"
            cP::Array{Float64, 2}
            HHsol(; aP = fill(1.0, HYPname.na, HYPname.nz), cP = fill(1.0, HYPname.na, HYPname.nz)) = begin
                    #= C:\Users\kittisopikulm\.julia\packages\Parameters\MK0O4\src\Parameters.jl:493 =#
                    HHsol(aP, cP)
                end
            HHsol(aP, cP) = begin
                    #= C:\Users\kittisopikulm\.julia\packages\Parameters\MK0O4\src\Parameters.jl:505 =#
                    new(aP, cP)
                end
        end
    ()
    ()
    begin
        HHsol(pp::HHsol; kws...) = begin
                (Parameters).reconstruct(pp, kws)
            end
        HHsol(pp::HHsol, di::(Parameters).AbstractDict) = begin
                (Parameters).reconstruct(pp, di)
            end
        HHsol(pp::HHsol, di::Vararg{Tuple{Symbol, Any}}) = begin
                (Parameters).reconstruct(pp, di)
            end
    end
    function Base.show(io::IO, p::HHsol)
        if get(io, :compact, false) || get(io, :typeinfo, nothing) == HHsol
            Base.show_default(IOContext(io, :limit => true), p)
        else
            dump(IOContext(io, :limit => true), p, maxdepth = 1)
        end
    end
    macro unpack_HHsol(ex)
        esc((Parameters)._unpack(ex, Any[:aP, :cP]))
    end
    begin
        macro pack_HHsol()
            esc((Parameters)._pack_new(HHsol, Any[:aP, :cP]))
        end
    end
    HHsol
end

Now we can turn that function into a macro:

julia> macro fill_params(s, e)
           esc(fill_params(e, s, __module__))
       end
@fill_params (macro with 1 method)

julia> @macroexpand @fill_params(HYPname,struct HHsol
           aP::Array{Float64,2}
           cP::Array{Float64,2}
       end)
quote
    begin
        $(Expr(:meta, :doc))
        struct HHsol
            "Default: fill(1.0, HYPname.na, HYPname.nz)"
            aP::Array{Float64, 2}
            "Default: fill(1.0, HYPname.na, HYPname.nz)"
            cP::Array{Float64, 2}
            HHsol(; aP = fill(1.0, HYPname.na, HYPname.nz), cP = fill(1.0, HYPname.na, HYPname.nz)) = begin
                    HHsol(aP, cP)
                end
            HHsol(aP, cP) = begin
                    new(aP, cP)
                end
        end
    end
    ()
    ()
    begin
        HHsol(pp::HHsol; kws...) = begin
                (Parameters).reconstruct(pp, kws)
            end
        HHsol(pp::HHsol, di::(Parameters).AbstractDict) = begin
                (Parameters).reconstruct(pp, di)
            end
        HHsol(pp::HHsol, di::Vararg{Tuple{Symbol, Any}}) = begin
                (Parameters).reconstruct(pp, di)
            end
    end
    function Base.show(io::IO, p::HHsol)
        if get(io, :compact, false) || get(io, :typeinfo, nothing) == HHsol
            Base.show_default(IOContext(io, :limit => true), p)
        else
            dump(IOContext(io, :limit => true), p, maxdepth = 1)
        end
    end
    macro unpack_HHsol(ex)
        esc((Parameters)._unpack(ex, Any[:aP, :cP]))
    end
    begin
        macro pack_HHsol()
            esc((Parameters)._pack_new(HHsol, Any[:aP, :cP]))
        end
    end
    HHsol
end

We can then use it as follows.

julia> HYPname = (; na = 5, nz = 3)
(na = 5, nz = 3)

julia> @fill_params(HYPname,struct HHsol
           aP::Array{Float64,2}
           cP::Array{Float64,2}
       end)
HHsol

julia> HYPname = (; na = 5, nz = 3)
(na = 5, nz = 3)

julia> HHsol()
HHsol
  aP: Array{Float64}((5, 3)) [1.0 1.0 1.0; 1.0 1.0 1.0; … ; 1.0 1.0 1.0; 1.0 1.0 1.0]
  cP: Array{Float64}((5, 3)) [1.0 1.0 1.0; 1.0 1.0 1.0; … ; 1.0 1.0 1.0; 1.0 1.0 1.0]


julia> HYPname = (; na = 4, nz = 9)
(na = 4, nz = 9)

julia> HHsol()
HHsol
  aP: Array{Float64}((4, 9)) [1.0 1.0 … 1.0 1.0; 1.0 1.0 … 1.0 1.0; 1.0 1.0 … 1.0 1.0; 1.0 1.0 … 1.0 1.0]
  cP: Array{Float64}((4, 9)) [1.0 1.0 … 1.0 1.0; 1.0 1.0 … 1.0 1.0; 1.0 1.0 … 1.0 1.0; 1.0 1.0 … 1.0 1.0]```
3 Likes