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.

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

3.

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.

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.

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.

9.

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

10.

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.

11.

> 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.

12.

It's funny how little developers think about how to do configuration right.

It's just a bunch of keys and values, stored in some file, or generated by some code.

But its actually the whole ball game. It's what programming is.

Everything is configuration. Every function parameter is a kind of configuration. And all the configuration in external files inevitably ends up as a function parameter in some way.

The problem is the plain-text representation of code.

Declarative configuration files seem nice because you can see everything in one place.

If you do your configuration programmatically, it is hard to find the correct place to change something.

If our code ran in real-time to show us a representation of the final configuration, and we could trace how each final configuration value was generated, then it wouldn't be a problem.

But no systems are designed with this capability, even though it is quite trivial to do. Configuration is always an after-thought.

Now extend this concept to all of programming. Imagine being able to see every piece of code that depends upon a single configuration value, and any transformations of it.

Also, most configuration is probably better placed into a central database because it is relational/graph-like. Different configuration values relate to one another. So we should be looking at configuration in a database/graph editor.

Once you unchain yourself from plain-text, things start to become a lot simpler...of course the language capabilities I mentioned above still need to become a thing.

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.

> [In Chapel] there’s the config keyword. If you write config var n=1, the compiler will automatically add a --n flag to the binary. As someone who 1) loves having configurable program, and 2) hates wrangling CLI libraries, a quick-and-dirty way to add single-variable flags seems like an obvious win.

Letting people define configurable variables at their call site is incredibly valuable, even if you don't have compile-time support, and even if you're working on something not meant to be an isolated binary.

At my startup, one our most beloved innovations is that you can write `resolve_config("foo", default="bar", request=request)` pretty much anywhere you'd normally hardcode a value or feature flag... and that's it.

The first time it's seen in any environment, it thread-safely inserts-if-not-present the default value into a key-value storage that's periodically replicated into in-memory dictionaries that live on each of our app servers. Any subsequent time it's accessed, it's a synchronous key-value lookup in memory, with barely any overhead. But we can also configure it in a UI without needing a code redeploy, and have feature flags and overrides set on a per-user or per-tenant basis.

Sometimes, you don't need language support if you have some clever distributed-systems thinking :)

25.

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.

26.

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

27.

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

28.

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' });

29.

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.

30.

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

31.

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.

32.

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.

33.

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.

34.

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.

35.

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.

36.

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)".

37.

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.

38.

When JavaScript added hash/object deconstruction (both at the argument level and assigning variables) I noticed code has been using Dict-like function arguments everywhere. It makes typing them a bit more of a pain in the ass (especially without default arguments).

I haven’t decided if I like it better than just breaking up objects into arguments in a more simple functional style.

On one hand it’s more predictable but on the other most complex apps start passing around objects for everything. Typescript of course helps with that, as does nearly modularized code (ie not passing in full typed objects outside of the parent module which owns/generates them unless they uniquely operate on the full object).

These are the small rescissions you end up making a hundred times.

39.

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

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

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

40.

Why not put it in code? You're program in strong typed lang, you write the config in that lang. You have a config function (analogous to the file) and a config "data" type (returned by the function, specified by the "configuree"). The function can only read env vars (keys, secrets, etc) and return the data structure layout by the app, strong typed enough you can prevent any side-effect easily by restricting the return type of the config function.

Your IDE can help you write only acceptable config files (functions) this way.

In case you do not want to recompile for conf file changes, many languages come with some kind of interpreter. You may even make it hot-reloadable for some properties.

You need more, you probably need a config service, which you can build in a type safe fashion as well.

41.

The second one is impractical because the number of classes you'd need would increase exponentially with each added flag. The first one clashes with

> Prevent over-configurability,

because you're accepting an infinite range of possible callback methods instead of asking a simple yes/no question.

I don't get why this rule even exists, actually. If you take three boolean arguments, that's not okay, but if you wrap them all into a configuration object and pass that instead, that's suddenly... okay?

42.

> 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.

43.

> 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.

44.

> They lay operational traps everywhere in their quest to get things done fast (like directly embedding config data in code to avoid fixing the config format).

Specific to your example, rather than the sentiment, I think embedding config in code is highly valuable when you don't have a lengthy deployment cycle and have direct access to the source. It gives your compiler more information which can help prevent bad configurations (which are the cause of failures more often than anything else from what I've seen). Developers also have a better shot at feeling how badly configurable a particular component is when the configuration is code. It's much easier to hide the overly-configurable systems under a rug when the configuration is far away from implementation, in a DSL, in a different repository, or only visible at deployment.

45.

The problem with environment variables as configuration is that it's unstructured, hardly documented, and overall hard to reproduce and inspect.

Nothing is worse than trying to understand an issue with a program that heavily relies on environment variables for configuration, as environment variables are designed to be short-lived, memory only.

A good old configuration file is the best. You can version it, distribute it, it's explicit, possibly structured, easily documented.

That doesn't mean that there is no room for environment variables, but these should be for local-only hacks and tweaks.

46.

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++).

47.

Passing other parameters into the composed functions.

48.

You can use objects to imitate named parameters in JS/TS. I think that's widely used convention and most of my APIs use one object parameter instead of multiple separate parameters exactly because named parameters are awesome. With TS it looks clunky with all those type declarations, but I can live with that.

As to your question, I share this feeling. Naming parameters must be standard feature in every language. Absolute majority of functions would benefit from verbose calling syntax.

49.

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.

50.

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?

51.

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).

52.

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

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 my own applications, I've moved away from configuration files as much as possible. Instead, I provide an API and all my configuration is just code.

I think creating an entirely new Turing-complete language just for configuration is a waste of brain cells for everyone involved.

55.

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

56.

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.

57.

I like parameter objects a lot these days, they are more verbose to set up initially but they make function calls descriptive and the parameter order irrelevant, I think this is really good for developer usability.

I dislike parameters within the URL now, when you consider the usability of Swagger and browser debug consoles a URL like `/api/update-thing?id=foo` is much nicer for non-developers, whereas `PATCH /api/thing/:id` is built on understanding REST conventions and generates ambiguous requests in browser tools.

58.

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

60.

Using globals is simpler, it's also pretty natural in event driven architectures. Passing everything via function arguments is welcome for library code, but there's little point to using it in application code. It just complicates things.

61.

Env vars are usually better than the alternatives, but orgs often move beyond them and build their own configuration systems. Why?

One big one is that the way environment variables work fundamentally encourages a small number of variables, which is at odds with the desire to have a highly configurable system.

62.

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.

63.

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.

64.

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?

65.

>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?

66.

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

67.

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 :)

68.

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.

69.

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.

70.

The same is achieved with Java's use of single-method-interfaces. It doesn't matter what the method is called, it can be used in function contexts without referencing the specific method name.

I'm not sure I'd like my functions to have properties (which gives them state and can alter what calling it does with the same arguments). A big benefit of FP is getting away from OO states. Perhaps the problems I work on aren't complex enough to benefit from them, or I simply make objects from classes.

71.

I think the advantage for simple objects is just that you can refer to the columns directly vs having to go through a function. It's only really a big win if you're manually writing a lot of sql or if you're using something like Rails and you want to write queries against values in the JSON blob without going through a bunch of hoops.

72.

configuration as code.

this is all code.

73.

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.

74.

configuration as code in python is perfectly fine and no issue at all: express the right data structure to map you problem domain well, and you're done.

75.

> "CLI args are usually passed around explicitly" -- I think this is a pro, not a con.

Sure; I never said it's a con. They have different characteristics, and are both useful in certain situations :)

> I think the correct term for "things the caller knows better than the implementor" are parameters.

True; that's also the name Racket gives to dynamically-scoped variables https://docs.racket-lang.org/guide/parameterize.html

In fact, Racket uses a parameter (dynamically-scoped variable) to store the environment. This is actually slightly annoying, since the parameter is one big hashmap of all the env vars; but I usually want to override them individually. One of my Racket projects actually defines a helper function to override individual env vars makes a copies all the other environment ( made a are contained in a parameterhttps://github.com/Warbo/theory-exploration-benchmarks/blob/...

76.

Could you be more explicit, perhaps with an example? Do you simply mean you prefer function/constructor parameters over DI?

77.

> Objects should have a fixed set of keys.

Declared in a fixed order if you want functions to remain monomorphic! One case where using classes is a bit safer than object literals.

78.

Neither of those address the issue of scoped overrides of a setting. The benefit of the functional options approach is they return the inverse setting, so you can very trivially do scoped overrides for things like log levels. Eg:

prevVerbosity := foo.Option(pkg.Verbosity(3))

foo.DoSomeDebugging()

foo.Option(prevVerbosity)

(better yet, use defer to restore)

Your examples seem to be reducing the problem space to exclusively object creation time. And in that case, yes named params or a struct with default values work great. But they work a lot less great when you're talking about changing an existing object, as now your default values can't just be the actual default values, but rather optional values since you need to distinguish between "set to a value that happened to be the same as the default" and "didn't set a value at all, so don't change it".


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