Yet another "how to pass multiple parameters to a function" post

Apologies for the n-th post on this topic (#1, #2, #3, etc. etc).I simply don’t get it and feel overwhelmed by all choices of how to pass parameters to a function (positional arguments, optional keyword arguments, named tuples with varargs, Parameters.jl).

I am developing a package, which you call a function to estimate a model. This function takes in some parameters which have default values. Even after reading most threads on the topic, I don’t understand, where do I put the structure with parameters to pass along in functions? This is if I am not using keyword arguments.

Here is my current code for a file model.jl that uses my package: Load packages, load data, use the function.

using Revise
using BEAVAR
using CSV
using Parameters


Y_20 = readdlm("data/FRED_Chan2019.txt", ',');

@with_kw struct hypChan2020
    c1::Float64 = 0.04; # hyperparameter on own lags
    c2::Float64 = 0.01; # hyperparameter on other lags
    c3::Float64 = 100;  # hyperparameter on the constant
end

store_beta = Chan2020_LBA(Y_4,hypChan2020)

where my function looks like this:

module BEAVAR
function Chan2020_LBA(YY,hypChan2020;p::Integer=4,nburn::Integer=1000,nsave::Integer=2000)
...
end
emd

I would actually like to have a larger struct hypChan2020 and not have it in the file, while the user should still be able to change the parameters. I am currently achieving this (for other parameters) with keyword arguments, and I could put c1,c2,c3, but with many it gets cumbersome.

What is the best way to pass a structure, which is not in the main .jl file that gets called in the function and the user doesn’t see it? More accurately, my question is where should I put the strcuture?

  • I cannot put `@with_kw’ in the function directly because I get an error that macros cannot be in the local scope.

  • Should I have a separate file for the options and include it? E.g.

Y_20 = readdlm("data/FRED_Chan2019.txt", ',');

include('fileWithKWMacro.jl')

store_beta = Chan2020_LBA(Y_4,hypChan2020)
  • I tried putting the macro in the module but I also couldn’t get it to work and that would be weird as I would have different structures for different models and I would like each to be associated with the function that needs it.

Ideally I want the user to just load the data and choose the function, i.e. they should be able to write their own model.jl without the need to include a specific file or call Parameters.jl (which I call in the module later anyways for unpacking).

Edits: rephrasing the question for clarity

This should be inside your module, and the name of the struct should be public, probably exported. The user must create one instance of that struct, and pass it to the function.

In the example you are passing the type.

The function can or cannot set a default value for the parameters, with options::hypChan2020=hypChan2020() as a keyword or optional positional argument.

(This alternative is what I described here: How to allow users to safely change package "constants"? - #6 by lmiq)

3 Likes

You can also note that Parameters.jl is not necessary for structure unpacking. You can use this syntax

struct MyStruct
  x::Int
  y::String
end

s = MyStruct(1,"test")
(; x, y) = s
5 Likes

I see, i definitely didn’t export it when I tried putting it in the module and didn’t create an instance.

Thank you, I will try and report back.

Interesting, so this is an alternative to the @unpack macro? For the macro I do need to load the package, right? At least I get an error.

Could you tell me why there is semicolon in the last line? I don’t understand the syntax.

s = MyStruct(1,"test") creates a tuple, correct?
where can I read about the ; in (; x, y) = s?
Thanks!

@lmiq Okay, I think I am almost there.

So I added the struct definition hypChan2020 in the module and exported it. This allows me to neatly call the function without any arguments. Here is my syntax

function Chan2020_LBA(YY;hyp=hypChan2020,p::Integer=4,nburn::Integer=1000,nsave::Integer=2000)
end

then the user can just do

using BayesianVAR
Y_20 = readdlm("data/FRED_Chan2019.txt", ',');
Chan2020_LBA(Y_4)
Chan2020_LBA(Y_4,p=1,nburn = 100, nsave = 5000)

these all work and I also see hypChan2020() in the global scope.

However, I am struggling with the syntax from your referenced post.

For example, this doesn’t work

using BayesianVAR
Y_20 = readdlm("data/FRED_Chan2019.txt", ',');
hyper=hypChan2020()
Chan2020_LBA(Y_4,hyp=hyper())

and throws
ERROR: MethodError: objects of type hypChan2020 are not callable The object of type hypChan2020 exists, but no method is defined for this combination of argument types when trying to treat it as a callable object.
Also hyp is an immutable struct so I cannot change anything, but that is an easy fix.

The thing is that you are mixing the type definition with the instances of the type, and inside your function you are likely calling hyp() again, which, if hyp=hyper(), would be hyper()(), that is, you are trying to call an instance of the type hyper as if it was a function.

The correct MWE would be:

@kwdef struct Hyper
     a::Int = 1
     b::Int = 1
end

function model(x; hyper=Hyper())
     (; a, b) = hyper # equivalent to @unpack
     return x + a + b
end

And the user can then do:

my_options = Hyper(a = 2) # create an instance of Hyper changing `a`
model(1.0; hyper=my_options)

Note that, by default, the function sets hyper=Hyper() which calls the constructor Hyper(), meaning that it is creating a Hyper object with default parameters.

ps: You don’t need the struct to be mutable, the user will create a new, immutable struct, with the parameters they want.

1 Like

Actually, if you only use Parameters.jl for unpacking struct, then you can remove it from your dependencies since it is no more necessary. Actually, a struct creates a Type and not a tuple. Conversely, NamedTuples can be seen as anonymous struct.

2 Likes

Ah, I see, thanks you @lmiq and @Maucejo .

2 Likes