what are the advantages of using a configuration object for function parameters?

share

Summary of results

GPT-4o
Warning: quote links might not work while streaming
1.

With large numbers of parameters, it's almost always more readable to use a config struct. Especially since often, you want to collect configuration from multiple sources, and incrementally initializing a struct that way is helpful.

2.

Keyword arguments or named parameters solve this. In JS I tend to pass a single object to a function with a large number of parameters like this. But I do agree using named entities like enums or constants is also good. A bit heavier but if you do it everywhere the cost is worth it.

3.

I see a benefit from having this options container: you can have a central place to set the options and then only pass down the decoder and the user of the function doesn't have to bother with the configuration

4.

When used in conjunction with named parameters with default values, it makes it possible to write function signatures that are significantly more readable than those written in Java, and with far less boilerplate.

5.

In Java or C++ or other OOPish languages, say, you might make all your classes be Configurable (unless they are Configuration), which means their constructors would take a Configuration in some way, possibly with an explicit Configuration argument or with a Configurable argument whose configuration to copy. This way all your objects will know how to find configuration information.

6.

One of the advantages of functions is that a well-named function is self-documenting. If you can take a bunch of lines and wrap them in a function whose name summarizes exactly what it does, then you have improved readability in my opinion. In this example, I don't really need to know the details of how the query parameters are extracted. I just want to know I've got them.

7.

The essential convention over configuration ideas still prevail when you want to pile on functionality quickly and without boilerplate.

8.

I code in this style. I can tell a lot of things started going right when I adopted the style too. Instead of global variables I have a single config which I initially pass around to functions, then I refine and pass only the data each function needs. The config generally doesn't get mutated. Testing is easier. Functions are easy to reason about. I keep functions to a dozen lines or so, but not to the same neurotic level uncle bob presribes. Likewise a few extra arguments are fine, but too many usually signals a new type is required or time to compose a new function which utilizes the function whos parameters are growing.

9.

You don't suggest any solution. Do you want more function overloading or maybe config objects?

Adding default parameters works well with existing code. It is not bad and lazy because it is easy.

10.

Caller provided objects are a standard idiom that offers greater flexibility to use static/global vars, objects with a FAM, and custom allocators.

11.

I think the idea is that the constructor is for dependencies not parameters, things that are REQUIRED to be set at construction time for the sanity of the object. Anything that is optional can be set in the normal way after construction eg using Object.assign

If you have a situation where an object with many parameters must be configured with a certain (yet different) subset of parameters in certain scenarios, then you probably want a factory.

12.

> Either copy/paste or rolling them into config objects and passing those down is generally preferable

Preferable for whom? I do not prefer it. I much prefer to avoid the extra work it creates for me vs. the simplicity of kwargs. I use explicit args for the function I made and then add *kwargs on the end and then I don't have to write bespoke config objects or copy and paste a bunch of stuff that might be obsolete by a future update to some library and also pollute my function's signature. I would very much welcome a way to tell callers where kwargs is going without having to do extra work.

13.

Tangential but coding in JS/TS, I often will go for object arguments to make things more readable. If you have a function like:

foo(arg1: boolean, arg2: boolean, arg3: boolean)

Then when you call it it will look like foo(true, false, true) which is not great for readability. Instead I move all the arguments into an object which makes each field explicit. Ie.

foo({ arg1: true, arg2: false, arg3: true })

This also carries the advantage that you can quickly add/remove arguments without going through an arduous refactor.

14.

- If your configuration has more than 5-10 options then env vars become a mess while a configuration file is more maintainable

- Nested configuration / scoping is a mature advantage of configuration files

- You can reload configuration files whereas you can't reload environment variables during runtime

- A configuration file is a transparent record of your configuration and easier to backup and restore than env variables. Env variables can be loaded from many different locations depending on your os.

- In configuration files you can have typed values that are parsed automatically with env variables you need to parse them yourself. This is just a difference not that bad for env variables per se.

15.

Personally I still recommend the config file. Even when they are simple, it gives you one single source of truth that you can refer to, it will grow as you need it, and it can be stored in source control.

Where and how parameters are configured is a bit more of a wild card and dependent on the environment you are running in.

16.

Yes, but you can achieve that just by having a normal function that takes some configuration parameters. The documentation of the library suggests that efficiency was a significant concern, and I assume that’s why the implementation is not as straightforward as it otherwise might be.

17.

Do you ever find yourself taking an existing function and adding another parameter to it? The benefit is that you don’t break existing code. The problem is that the function is now more complicated and likely now does more than one thing because the extra parameter is effectively a mode switch.

18.

The "convention over configuration" can be regarded as self-referential. These guidelines are often good default choices in situation where you don't have a strong reason to go either way. You wouldn't write a program which is configurable between those choices (e.g. exhibits more code repetition or less based on a run-time setting): so you go with a good default convention. If you can avoid repeating yourself, that's usually good; machines should do the repetitive work rather than people. If you know that two or more repetitions of something are only initially that way and soon going to diverge, then might as well fork those copies now. Or maybe do allow yourself to repeat yourself, but via macro. Some compiler optimizations violate DRY by design: function inlining, and loop unrolling. It's invisible to humans who aren't disassembling the output, or measuring code size changes.

19.

The main benefit is you can have configuration options without having to specify all values, and also have non-zero-value defaults. Lets say you had something like Sarama's config struct which contains 50 or so config knobs. The following is will lead to some terrible defaults:

NewConsumer("kafka:9043", Config{ClientID: "foo"})

Here, with this config, there is a config option `MaxMessageBytes` which will be set to 0, which will reject all your messages. What Sarama does is, you can pass a `nil` config which will load defaults, or:

conf := sarama.NewConfig();

conf.ClientID = "foo"

conf.RackID = "bar"

NewConsumer("kafka:9043", conf)

and so on. This is ok but can be cumbersome, especially if you just need to change one or 2 options or if some config options need to be initialized. Also someone can still do &Config{...} and shoot themselves in the foot. The functional options style is more concise.

NewConsumer("kafka:9034", WithClientID("foo"), WithRackID("bar"))

I used to be a fan of this style, and I even have an ORM built around this style (ex. Query(WithID(4), WithFriends(), WithGroup(4))), but I think for options like these a Builder pattern is actually better if your intention is clarity.

20.

Functions are a nice to have, but:

- It tends to make things less declarative.

- You lose locality of behavior, which is very useful in configuration.

Also, nickel doesn't support injecting data into the nickel file, so external program can't set variables, query a database and pass the result to the conf file, etc.

21.

Either copy/paste or rolling them into config objects and passing those down is generally preferable. Copy paste doesn’t always feel great for pass through arguments but it’s perfectly interpretable.

Naked kwargs is so difficult to work with that I hesitate to think of a use case where it wouldn’t be an anti pattern.

22.

Also it’s easy enough to pass objects with named properties and destructure the object in the function parameters. So there’s no real need for named parameters anymore and it would just add another duplicative way of writing the same code.

23.

There's nothing anti-functional in bundling related functions into an object.

On the contrary, this helps with modularity, which is a good thing. You can then swap out the object for another object with the same interface, but where the functions (methods) have different implementations.

24.

There's many benefits in having a state machine configuration that can be serialized, especially if you can store that config in a database and load it at runtime. And actually xstate does allow you to pass the functions directly if you want to

25.

I like tools that have 1:1 mappings for config file and command line flags because, as you say, both have advantages and disadvantages.

26.

Not sure if I would call hardcoded settings in the sourcecode configuration - they are just constants. The primary benefit of configuration is the ability of changing the behavior of software without having to rebuild, redeploy, redistribute or even restart. Either by the user or by the developer or sysadmin, depending on context.

27.

you have to instantiate variables anyway. grouping those variables in objects simply makes your code more understandable - there is really no extra cost.

28.

When they're function parameters, you can write them as regular function declarations, btw. ;) Might hurt your memorization efforts though.

29.

Is that really such a common case? Obviously it depends what you’re configuring, but I definitely would not expect that it’s typically OK to jump in and modify the configuration of an object that’s already in use. What if it does do some expensive one-off setup using the supplied config at object creation time?

If you really need a general scoped override, it could be done in the config struct approach just by copying and restoring the entire config. This might be expensive if the struct is big, but on the other hand, you could change multiple properties at once which doesn’t look possible in the function-based approach.

30.

I generally prefer keeping all the configuration in as few languages as possible and preferably in a single language. Adding filesystem-based config where a config option object in the main language of javascript would suffice goes against that.

Also, given a filesystem config, now I'm forced to have many very small files around for each route where each file is most likely just a call to another service handler. I'd prefer to mash most into bigger files that handle related but distinct routes.

Less important, but comes up, it's nice to be able to match routes based on code and not just string equality ... e.g. everyone seems to like having routes for usernames start with '@'

31.

JavaScript has named parameters, in sort of the same way that C has named parameters.

Get used to writing your functions using objects for the arguments:

function myFunc({ foo, bar }) {}

Then you can call

function myFunc({ foo: 1, bar: "x" })

Similarly in C/C++

struct MyFuncArgs { int foo; char bar; };

void myFunc(MyFuncArgs args) {}

myFunc({ .foo = 1, .bar = 'x' });

32.

I think the way it's done is correct, an object passed as a parameter with key value pairs for attributes seems a lot more logical.

33.

Can you explain how having a global variable is more performant than passing a pointer to an object as a function argument in practice?

34.

I prefer Nim's approach where you can have objects, but they're just variables (properties). The procedures aren't tied to objects, but you can pass/return objects. To me this is more flexible.

I can write OO code as well, this is just personal preference.

35.

There's one advantage of foo.bar calling a function, it works well with referential transparency. Whether it's a property/value or function, only the result matters. I can't say it's a big difference though, I've only been mildly annoyed by having to change all call-sites when changing between them. Other languages allow code bodies for getters/setters (foo.bar = ...) so it still hides the call. For a C-style syntax language having the () seems less surprising.

36.

Creating a class around the too many arguments you want to pass to your function may be a good idea if the concept happens to be coherent and hopefully a bit more long-lived than just the function call.

Otherwise, your just hiding the fact that your function requires too many arguments by calling them properties.

37.

As an addendum if the function only needs the keys I would possibly just have the parameter be a string[] that expected the user to call object.Keys to pass to.

That way the function isn't asking for parameters it doesn't really care about.

Though I do get the appeal of having the function call object.Keys if it's called frequently so as not to have to sprinkle that call everywhere.

38.

Or you could just take the two parameters you're actually using on your function. No new type, no need to pass your mega-object, just take two nice strongly typed arguments.

39.

I think we're off on a tangent here, but I will at least agree that named parameters are a godsend in any programming language. Things are so much easier to read when the caller can clearly state their intentions for things like "foo(true, false, true)".

40.

Really, it’s because of the habit of Ruby programs to pass a single options (params, args, config) object in to avoid complex configuration.

Really, what’s needed is separation of concerns, instead of a single “do stuff” function that takes an “everything” argument.

But what’s really wanted is global variables with everything is a single scope.

41.

So in someways, the best generalization like this I already commonly see is a capabilities system, which can be done essentially via function parameters. I've used this as an OOP pattern once or twice and I think it's the easiest way to explain it:

Imagine you have 2 singleton objects in your 'void main()', redkey and bluekey of types Redkey and Bluekey.

Somewhere else, you declare your function: 'int foo(Redkey redkey, int x, int y)' that needs a Redkey object of which only one exists: in your main.

This by itself forces every call on the path between 'main()' and a call to 'foo()' to also include a parameter Redkey.

In the extreme cases where a pattern like this is useful (tracing IO calls, you named it) it can really help cut through a codebase after an hour of refactoring. But it can be limiting. Async and Checked Exceptions are probably the most colored functions and they both need escape hatches because of that.

42.

Named parameters are easy enough to mimic in JS using objects and the spread operation

function foo({bar, baz}) { ... }

foo({bar: 1, baz: false})

43.

> It's better to have some wonky parameterization than it is to have multiple implementations of nearly the same thing. Improving the parameters will be easier than to consolidate four different implementations if this situation comes up again.

Hard disagree. If you cant decompose to avoid "wonky parameters" then keep them separate. Big smell is boolean flags (avoid altogether when you can) and more than one enum parameter.

IME "heavy" function signatures are always making things harder to maintain.

44.

> The disadvantage with making the parameter class a member is that the code gets littered with indirection like 'data->param1' all over the place. This is much nicer.

Isn't that a rather trivial concern? Implementation inheritance is not "much nicer" than delegation, the opposite in fact is the case. It adds a dispatch step to all calls to virtual methods (including calls that are private to implementations at any level of your 'hierarchy') that is not what you would want in most cases. Which in turn means your entire class hierarchy has to be analyzed as a single, highly-coupled program module; it's quite literally impossible to understand portions of it in isolation.

45.

I find the concept of a context structure passed as the first parameter to all your functions with all your "globals" to be very compelling for this sort of stuff.

46.

I'm not sure the opposite of "configuration" should be called "convention" - the worst abuses I've seen of punting to user configuration have been ones where the best solution was only determinable at runtime. (e.g. compare user-configured fixed window sizes in the doomed ISO protocol stack with dynamic window control in TCP)

Typically the user doesn't know best - they copied a config that worked for someone else, years ago on a different machine and workload, and don't know what any of the parameters actually mean. In the worst case (sendmail?) you have O(0) people who actually know how to use the configuration language, and 10 competing higher-level config generators.

47.

what you need is really just Data structure + Algorithm

And Context (at least in my humble experience). Lately I got rid of OOP too and now all my functions accept `params` as a first argument which contains settings and the state of an object/subserver/etc. I find the ability to group functions into different files useful though (it’s js so I cannot do that like one does in c++).

48.

I've actually started preferring the `action(subject, object)` form of programming which OO in C entails, rather than `subject.action(object)`. The latter is certainly easier for discovery via auto-complete (with most current tooling).

OO is not a natural way of programming. Everything always starts simply with functions that take arguments. Then you have a function that calls another function with some of the old arguments and some new ones.

Most people go: let's make the shared param an instance var.

const config = {}

foo(config)

function foo(config) { bar('bar', config) }

bar(str, config) { ... }

becomes...

class MyClass {

config

constructor(config) { this.config = config }

foo() { this.bar('bar') }

bar(str) { ... }

}

The problem with a class, is that every method of the class potentially depends on every other member of the class. What usually happens is that stuff is added to the class that doesn't make sense. And every class needs a noun to name it. Then you have to think what is the proper name for this abstract thing you don't even know what it is yet. Which leads to all these quirky class names that are unnecessary.

If you are explicit about what data dependencies each function has, it becomes easier to see the commonality that should be extracted into classes. Most people just shove everything in a class too soon. And most languages push you to use classes and methods...which usually look very different to how functions are represented.

49.

Polymorphism is doable in plain old C with lookup tables and function pointers. If that is the only benefit, what is the point of creating a language where everything is an object?

50.

The two main advantages of using classes are:

a class is also a type (if you have a typed language),

a class is also a module, you group together values that makes sense together (the x and the y of a point) with the functions (methods) that interact with it.

When you have a lot of codes flying around, you can add encapsulation (private/public thingy) so the user of a code does not see the implementation which helps to create libraries that can evolve independently from the applications using them.

Also compared to an associative array, a class is more compact in memory (granted JavaScript runtimes see dictionaries as hidden classes).

51.

Just curious, but from a pragmatic view, what is the advantage of using anonymous functions instead of named functions for any mildly complex code?

52.

Oh that's nothing. In Java I came up with a way to pass config options using method references as keys, so you can write something like:

createFoo(with(FooOptions::enable, true), with(FooOptions::size, 7));

And processing the options involves serialising each method reference to work out what it is!

53.

"you better have the configuration defined in code" - you better do that anyway for anything that is used and will be around for a while :)

54.

In short: the power of declarative configuration management. Way less error-prone than imperative shell scripts.

55.

Maybe, but it usually has the opposite effect. If you have well-designed and named functions the indirection is a feature since it reduces the amount of context you need to remember and gives you a visible contract instead of having to reason about a large block of code.

56.

As a user, I always prefer the bespoke configuration file format, provided it has comments explaining what each configuration option does.

57.

The problem with using a general programming language for configuration is you end up needing to use that config in different places, in different contexts, and from different languages. So, you have to marshall out and marshall in that structure to some intermediate format.

You want it to be easy for humans to edit and grok, so you find a way to represent the core parts you care about as text and cordone off the general programming language to another area.

You're successful and the number of use cases you cover grows, so the size of that config grows.

And before you know it, you've invented YAML.

Whereas, if you use Cue instead of YAML it looks pretty similar - in fact some large subset of it will be parsed correctly by a YAML parser. But the difference is with Cue you can:

1. Validate the structures in your config

2. Deduplicate by referencing other values in your config (something you can't do in JSON/YAML).

3. Use language built-ins to reduce boiler plate and repetitive text.

58.

except it's not just for function arguments/parameters. It can be used in all sorts of contexts to spread out an array or object. E.g.

```js

const object1 = {

field1: true,

field2: 42

};

const object2 = {

...object1,

field3: 'the meaning of life, the universe, and everything'

};

```

59.

This seems pretty iffy for introducing static/persistent variables to a function. I mean, it can work, but it's semantically very confusing. Parameters are part of a function's interface. A global variable would be much better.

60.

Fair. There are all those "configuration management" frameworks that make templating out config files from data a fairly solved problem, but if you're launching programs from other programs that you also wrote, I guess it's less code to have an arbitrarily long arguments list vs. also dealing with files.

61.

All of the things you said apply equally well to function parameters.

62.

If the method takes so many arguments that named parameters is necessary for readability, it is often a sign that the method is either too complex and should be split up, or that the method should accept a "config" record as a parameter. I this makes it clearer because you can group the parameters and give them meaning. Then you know that "url", "headers" and "body" belongs to the "request" record, while "customerId" and "taskId" are separate

64.

It's important, but I purposely stepped around this to not complicate things.

In truth, the only likely situation from these rules is hand-copying configuration then manually transposing. This isn't likely to happen in really hot code paths.

This is the one place where JS helps us because lazy typers are encouraged to return object literals from factory functions and the return will almost always be one object returned at the bottom of the function.

It also (intentionaly) bypasses manually adding elements to an object directly after the object is instantiated. In truth, doing this consistently and directly after it is created can usually be optimized too, but not necessarily and describing this also complicates things (and varies from one JIT to the next).

65.

a big plus one for both these items mentioned. and annoyance I've encountered with some configuration files is the lack of a built-in comment line so I can communicate in line with the configuration decisions to a future user.

Your comment about variables for usable values reminded me that it would be nice if the configuration file parser could reference other variables and sections of the configuration file to avoid repeating oneself in the same configuration file. for example, if the configuration files describing a fleet of virtual machines, it would be very handy to define small/medium/large system types with varying RAM, CPU counts, and disk layouts, then reference just the small, medium, or large type throughout the configuration file.

66.

Objects are a useful idea. Object Orientation is a terrible, terrible idea.

Functions + data (immutable when practical) is all you need for 90%+ of programming, and you should only reach for objects when necessary to manage some well-encapsulated but tricky state. Your default orientation should certainly not be towards objects.

67.

Is this common in python to declare local variables as fake parameters? The downside is obvious: it clutters the function interface with red herrings. What is the upside?

68.

>Your comment about variables for usable values reminded me that it would be nice if the configuration file parser could reference other variables and sections of the configuration file to avoid repeating oneself in the same configuration file.

How do you suggest specifying a variable?

69.

There would still be reasons to have configuration structs if we had named arguments.

70.

I'm with you re. short functions. Setting boundaries around code, i.e. scope, and giving that behaviour a well-defined signature (i.e. a descriptive name, what it needs, and what it gives back) makes it easier to reason about the overall functionality. Giant functions reduce my confidence that any given change isn't going to introduce a problem.

I like to imagine each execution path through a function as a piece of string. The fewer strings, the fewer kinks, and the less string overall, the easier it is for my monkey brain to handle most of the time. Yes this is 'cyclomatic complexity', but strings are nicer visually :)

71.

It's primarily a technique used in object oriented programming. So it's hard to translate to Haskell.

The big picture of what a DI framework does is let you declare your structural object graph using a config file or decorators and have the whole thing instantiated at runtime automagically.

The detailed view is "a fancy way to pass something like a function argument". An object that has dependencies gets them passed in ("injected") at runtime rather than calling dependent function directly or internally instantiating dependent objects and calling them.

Doing things this way in OO languages has a number of benefits, including improved testability.

72.

What does this comment mean? Function parameters used or not are part of the function signature, so obviously would participate in any form of argument dependent lookup.

73.

This type of problem seems to be the exact reason why Object Orientated programming was developed in the first place. Whenever I'm repetitively passing arguments from one method to another, I usually find an object hiding in plain sight.

74.

I agree, configs in code are not a bad thing necessarily if they don't change often or at all. Sprawling config files are a significantly higher challenge to get your bearing with.

75.

That’s really nice. One of my favourite uses for returning closures is to separate a function into ‘configuration’ and ‘execution’ stages. It becomes extremely useful when you have a ton of parameters but only a few change between typical invocations.


Terms & Privacy Policy | This site is not affiliated with or sponsored by Hacker News or Y Combinator
Built by @jnnnthnn