It's a pretty convincing fake, but it has one problem: the person who wrote the function needs to have written it that way. What I wish more languages had (C++, Java, etc.) is the following:
Suppose I write some function that take way too many parameters:
void foo(char a, short b, long c, float d, double e){}
What I want, as a caller, is for the language to let me get a struct corresponding to the argument list, something like this:
foo::args whatever;
whatever.e = 3.14;
// fill in the rest
And then add a bit of magic syntax so we can "unpack" that struct to actually call foo. (Extending this proposal to handle varargs is left as an exercise to the reader, because I have no idea.)I see two possible implementations:
- either the struct is replicated at the function call (not really efficient, but the semantics are straightforward);
- either the struct is used to prepare directly the stack for the function call (more efficient, but the semantics must be defined).
One of the issues of the second approach is that if the structure/variable is a classical one, what should happen if we use the variable after the call?
If we have to read the values inserted before the call then we have to keep a copy of the structure in the stack (so it's the same as the first approach). Or we can consider that the variable no longer exists in the scope. Which corresponds to the ownership concept in Rust for example. (but, to my knowledge, this does not exist in C++)
What is the best option? Or are there other options?
Another question, what sizeof(foo::args) should return ?
struct foo_args{char a; short b; long c; float d; etc...}
and then asked for sizeof(foo_args).
It might be a bit verbose, but I think I would still prefer this over native syntax for unpacking.
1. I need to handcraft the tuple type myself, and make sure it matches the function declaration. (In other words, I don't want to manually write tuple<char, short>. I want to write foo::arg_tuple.)
2. This provides no support for named parameters. Tuples in C++ are numbered, not named.
In general, it'd be really nice to have compile-time access to compile-time function info like this. Arguments, their types, etc.
Also they can lead to a poor pattern which is functions/methods that take too many parameters.
They also tend to make generics harder to understand as the possible parameters can vary. Of course you can make the same problem in C++ via, say, `auto multiply(int x, int y)` and `auto multiply(double x, double y, enum rounding rp = rounding::nearest)`
In general they impose the verbosity (in keyboarding and reading) in the routine cases but don’t free you from having to look up the argument semantics of lesser-used functions.
Side point: I used “keyboarding” because the obvious word, “typing” has a homonym which made the sentence confusing!
I understand how this is a problem but I’ve seen more than one function in C that takes 4 or more parameters.
Methinks it’s more a product of being an inexperienced coder and not being able to model a domain appropriately. The huge benefit? If you do need 4 parameters you can at least read them / understand them if they are named!
Would love to see you flesh this out a bit more
CreateProcess(NULL,cmd,NULL,NULL,TRUE,CREATE_NO_WINDOW,NULL,NULL,&si,&pi);
is a terrible interface that we should try to split up better, but CreateProcess(
lpApplicationName: NULL,
lpCommandLine: cmd,
lpProcessAttributes: NULL,
lpThreadAttributes: NULL,
bInheritHandles: TRUE,
dwCreationFlags: CREATE_NO_WINDOW,
lpEnvironment: NULL,
lpCurrentDirectory: NULL,
lpStartupInfo: &si,
lpProcessInfo: &pi,
);
tends not to attract the same scrutiny for some reason. On the other hand, removing named parameters doesn't actually fix this (see jasode's example at https://news.ycombinator.com/item?id=24401913 so I don't need another dozen lines), so it's not clear that this is a good argument against named parameters.And in fact in that case using a keyword isn’t that different from binding the meaning into the function name (`divide_rounding_down` and so on).
But in my experience I saw a pernicious pattern: that the function name would essentially become the common entry point for a large number of divergent functions `divide(dividend: x, divisir: y, truncate: true, underflow_handling: ufh::throw, negative_permitted: true)` and so on (contrived example for explanatory purposes).
In addition, positional arguments are simply easier to read; as keyword arguments are not ordered, you’re essentially parsing the arguments, seeking the important ones etc. You’re increasing the cognitive overhead of the common case while only marginally assisting the uncommon case. It’s like reading while hearing the sound of the words in your head: quite possible, but significantly slower than having the meaning enter directly.
But those are non-trivial or cumbersome in their own way, and often people don't for their own internal functions. Named parameters neatly side-steps the issue entirely, by rolling the "local constant" right into the function call itself.
Like consider these two python snippets:
['Ford', 'BMW', 'Volvo'].sort(true)
vs. ['Ford', 'BMW', 'Volvo'].sort(reverse=true)
The latter is much more readable than the former, and there wasn't any "too many parameters" issue. It's a way to help create self-documenting code that's also compiler enforced. And it absolutely frees the reader up from needing to go lookup what the boolean parameter on "sort" means.For the above you'd have an 'rsort' or 'reversesort' function instead (or if your language supports it, some fluent kind of sort().reverse()). If you have a lot of options, some of them boolean, you might send in an options struct or have an enum as mentioned in the article.
list.sort(true /* reverse */);
This convention is very handy for certain Win32 functions that take lots ofparameters, such as CreateProcess or CreateWindowEx. ['Ford', 'BWM', 'Volvo'].sort(`reverse)
TypeScript can do that with strings: https://www.typescriptlang.org/docs/handbook/literal-types.h...1. Clearly communicates the purpose of the variable
2. Means that order of parameters is not important
3. Makes the internal interface largely compatible with its JSON web service interface
It's been about 10 years since I coded any Objective-C and I do agree that syntax was an oddbal.
Never seen that as a tendency in languages with named params.
I have, however, seen functions / methods in languages without named params that take too many arguments and one has to guess what they are...
function foo({arg1, arg2, arg3}:{arg1: Thing1, arg2: Thing2, arg3: Thing3) {
...
}IMHO this sort of named arguments "easter egg" is fine for big "option bag structs" passed to functions that are not performance critical (or for small struct <= 16 bytes). For other cases, regular args are better.
OTH it seems like passing normal args also spills over into the stack very soon (only 4 registers used?):
https://www.godbolt.org/z/Mz9MTG
The main difference seems to be that normals arguments "spill over", while passing structs by value puts everything on the stack once the threshold size is passed.
> If the size of an object is larger than eight eightbytes, or it contains unaligned fields, it has class MEMORY
> [...]
> If the size of the aggregate exceeds a single eightbyte, each is classified separately. Each eightbyte gets initialized to class NO_CLASS.
So that only happens if the whole structure is bigger than 64 bytes. Otherwise everything gets passed in registers like normal.
However in my experience there are human factors problems which I posted in another comment.
It makes everything so readable. I can come back to my code a few weeks later and I don’t really need to look up function signatures.
In particular, my favorite thing about this in Objective-C is that the C functions have a different way of being called so you can easily distinguish between the differences in paradigms.
I wonder what other languages force all calls to be via named parameters. Would love to have another option :)
I guess in Python you could l’ont your codebase to require keyword parameters or opt-in positionals.
open(/port/ 127)
a) have default parameters in any parameter position. I.e. for this to be legal:
foo(int x = 0, int y, int z = 0) {}
b) to allow me to invoke a function like this: foo(,) //x and z are passed as default values:
c) or like this using named parameters: foo(x = 5, y = 1,)Also, some might argue that what you're describing (call sites not becoming invalid when you update the function) is a feature, not a bug. This way, you can add options to your functions and still have everything compile like it should.
I wouldn't personally argue that though, i think it leads to bad practice where functions take a bazillion options and they become horrible tech debt. I'm working in a mixed C++ and Lua codebase, and the Lua codebase is littered with functions that take, like, 9 different arguments that change the behavior of the function. When I asked one of my colleagues about this style, he said it was one of his favorite features of Lua, that you can add arguments to a function to change the behavior in certain cases but keep the original functionality (guarded with default values or if's or whatever). It's made much of the code a living nightmare!
So, I guess, in the process of writing this comment I've come over to your side :) say no to adding arguments with default values!