Common pattern for long list of configuration arguments


#1

I have multiple functions with a long list of configuration arguments.
Calling those functions makes a lot clutter visually:

filteredValue = function1(someValue, cfg1, cfg2, cfg3)
finalValue = function2(filteredValue, cfg1, cfg2, cfg3, cfg4)

I wonder if there is common pattern in functional programming which overcomes this problem. Two possibilities come into my mind:

  • Put all configuration variables in a configuration type with several subtypes and only pass this config type to the function:
immutable Filter1
    cfg1::Float64
    cfg2::Float64
end
immutable Filter2
    cfg2::Float64
end
immutable Cfg
    filter1::Filter1
    filter2::Filter2
end
...
filteredValue = function1(someValue, cfg)
finalValue = function2(filteredValue, cfg)

However in this case the function has to know the structure of cfg, which is not at all ideal.
The other method that comes into my mind is:

  • Put all configuration values in a type as shown above, but instead of passing that to the function itself, create an extra function, which initilizes the function in need using closures:
function initFilter1(cfg)
    return X -> function1(X, cfg.filter1.cfg1, cfg.filter1.cfg2, cfg.filter2.cfg3)
end
function initFilter2(cfg)
    return X -> function2(X, cfg.filter1.cfg1, cfg.filter1.cfg2, cfg.filter1.cfg3, cfg.filter2.cfg4)
end
...
filter1 = initFilter1(cfg)
filter2 = initFilter2(cfg)

filteredValue = filter1(someValue)
finalValue = filter2(filteredValue)

The cost of this method is the additional function that is needed for every function with a long list of arguments. Is this a common pattern in functional programming?
Are there other patterns?


#2

However in this case the function has to know the structure of cfg, which is not at all ideal.

Why not? It seems like you just need a constructor for your Config type which provides defaults.

type Config
    c1::Float64
    c2::Float64
    c3::Float64
end

Config(;c1=1.0, c2=2.0, c3=3.0) = Config(c1, c2, c3)

function function1(someValue, c::Config)
    # do stuff
end

filteredValue = function1(someValue, Config(c2 = 5.0))

#3

Putting it in a struct is probably the best way. Also see


which makes it even easier.


#4

In my case the Config type holds a lot of different types. Only a small part of the large configuration is relevant for the specific functions mentioned above. Unfortunately it is not just one subtype which I can pass to these function.

But my real concern with just passing the whole configuration to the function is the seperation of concern principle: https://en.wikipedia.org/wiki/Separation_of_concerns
What I mean here is that the function should not be concerned about the structure of the configuration variable. If it is, it is much harder to reuse that code in a different context, where I have a different set of configuration names and values, or no Config type at all.


#5

If you are talking about positional input arguments, just wrap the inputs in a tuple and splat them in:

cfg = (cfg1, cfg2, cfg3, cfg4)
finalValue = function2(filteredValue, cfg...)

If they are named keywords, use a Dict:

cfg = Dict(:cfg1=>blah, :cfg2=>bleh)
finalValue = function2(filteredValue; cfg...)  # note the semicolon

#6

Should note that Dicts are quite slow, and so if you know what the fields will be in advance, you should use a (immutable) type instead. Parameters.jl as mentioned above makes this easy to do. But yes, the choice is really between tuples, types, or Dicts.