As pointed out in some of the article's comments, a much better solution would be if all languages allowed putting the name in front of function parameters (and while at it, also not enforce a specific parameter order and skip default-value parameters).
A workaround in C99 (and more limited in C++20) is to use a single struct which bundles all the function parameters, and then use designated initialization, this also makes function paramaters optional, and at least in C99 they can appear in any order:
This is mainly useful for functions which take many parameters. One downside in C99 (but not C++) is that there's no way to define default values for struct items (other than zero/false).
In Julia you have positional and keyword arguments, separated by a semicolon.
function f(; a, b)
a+b
end
Default values are optional. This can only be called with named arguments like f(; a=5, b=7). Unfortunately the separation isn't required to be explicit when calling the function, so calling f(a=5, b=7) also works. Generally calling functions is extremely permissive (e.g. a function with two positional and two keyword arguments function g(a, b=5; c, d=8) can be called with keywords and positions interleaved g(1, c = 7, 5)), leading to potential confusion at the call site. Our coding guidelines enforce separation of positional and keyword at call sites, and with that additional restriction I have found Julias behaviour in this regard very pleasant. E.g.:
calc_something(a, b; bool_flag=true)
is the best style for this type of thing that I have seen.
In C++ I’ve gotten in the habit of `enum class is_gain : bool { no, yes };` so the call site is `v = calc_formula(ia, ib, is_gain::yes);`. An advantage is that such stron Boolean-like types can be passed repeatedly and maintain their type safety.
Except this isn't an issue that should be solved at the IDE level. Not everybody is using the same IDE and has all the same features, or even the same options enabled in an IDE.
The solution provided in the article is the way to go.
> I’d like a language that can contextually resolve enums, so I can just type something like calc_formula( ia, ib, is_gain ).
Swift does this and it should be considered for any new language design. Enum.variant can be shortened to .variant, including for variants with data in them, .variant(arg). Perfect solution, because the dot is an autocomplete trigger.
In Erlang, one would usually pass atoms like 'with_gain' or 'without_gain', and substitute default values with arity-overloading: e.g. there would be two calc_formula functions, one with 2 arguments and one with 3 arguments, and the 2-argument one would simply call the 3-argument with the last parameter set to 'with_gain'.
And in case of really large number of parameters, one would generally pass either a map or (in older code) a proplist: #{is_gain => true, other_param => 42, ...} or [{is_gain, true}, {other_param, 42}, ...]. There is no beautiful syntax for destructuring all that stuff with default values, unfortunately.
My rule of thumb these days in JS/TS is all functions with more than 2 parameters should be refactored to a single object parameter using destructuring. I don't start with a single object because most times YAGNI applies.
I agree that using booleans like this can be confusing. But, imo, it's more confusing to have a bunch of wrapper functions that create abstraction madness.
I mostly write computation/math-related code and I find using named arguments to be a good practice. This is also quite similar to OP's enum approach, e.g. sth like `calc_formula(a, b, is_gain=True)`.
To be fair, the older I get, the more I like explicit arguments for everything like in Swift (and smalltalk iirc).
Named parameters misses the point. Functions should do one thing and only one thing. This is why we have cos, sin, and tan and not a universal function for trigonometry: trig(mode='cos', ...). Such functions often become cumbersome to use since they are essentially multiple functions baked into one.
I do agree with you completely. But the problem here is as old as programming itself. It's not always easy to define function boundries.
If your function returns pizza, as mentioned in other comment, adding some toppings won't change the boundaries. It still returns pizza, with peperoni or not. It doesn't change how you make the pizza. You're just adding more data to it. You may solve it with syntax, language data structures, etc. Whatever you like to make it more readable to the caller. But probably you'd want to pass toppings in arguments.
On the other hand if you have a boolean param that changes how function works. That's questionable in my opinion. Say you want to return list of users from DB but omit interns, sometimes, and you need to call API (or query DB) to know if someone's intern. You could define `omitInterns` bool argument but it seems clunky to me.
I may be mistaken, though. As said: defining boundries is not easy.
This also touches other problem a bit. Should we strive to decrease `if` branching in our functions or not? I personally tend to branch very early on, so later I can follow straight path. That's not always possible, but if it is, it helps greatly. Makes code easier to follow.
A valid use case for functions with many arguments are setup/init/creation functions which sometimes take dozens of configuration options. It's definitely better to do such setup operations in a single 'atomic' function call than spreading this out over dozens of configuration functions where it isn't clear when and in what order those should be called, or a combinatorial explosion of atomic setup functions, one for each valid combination of setup options.
In Delphi I try to use sets over boolean parameters. Then I can easily add new possible set members instead of introducing more parameters.
type FuncParam = (fpDoThis, fpDoThat);
type FuncParams = set of FuncParam;
function MyFunc(arg1, arg2: integer; params: FuncParams): integer;
begin
result := 0;
if (fpDoThis in params) then
result := DoThis(arg1);
...
end;
// supply directly
MyFunc(123, 42, [doThis, doThat]);
// or indirectly
params := [];
if (condition) then
Include(params, doThat);
MyFunc(111, 222, params);
Delphi ain't the most elegant language out there, but the set functionality is pretty nice.
In a language with only ordered arguments, sure, boolean arguments are generally unreadable, but so is more than one parameter generally unless they are logically equivalent (the arguments to an add function), following a convention from some other context (e.g., the arguments to an divide or subtract function), or each unique in type in a way that they could only have on relation to the function (e.g., the iterable and function arguments to a map function; you may have to work to remember the order when writing, but when reading the meaning should be clear.)
With keyword arguments, this problem goes away, and not just for boolean arguments but for arguments generally.
This reminds me of a time I worked with Typescript, and the team kept reopening discussions around the use of the "Any" type.
Trying to mitigate the boolean param dilemma, I would lean on Erlang and its multiple function signatures. It tends to force your solutions into initially harder but eventually more graceful form.
Generally, when my code starts showing these kinds of warts (silly parameters getting tagged on to function signatures), I take it as a sign that the initial tack I've taken no longer addresses the problem I'm trying to solve. More often then not it goes all the way back to an incomplete / outdated understanding of the business logic.
It is a bane of numerical code, in which there are many flags, quite a few parameters (with values 0. or 1.). Moreover, some flags are conditional (e.g. the value of the argument 5 matters only if the flag at position 3 is true).
DEFVAR declares a variable to be special. That causes ALL uses of that variable to use dynamic binding.
The function inside a LET with a dynamic variable does not create a closure. If one calls CALC-WITHOUT-GAIN later, there is no binding - unless there is another dynamic binding of that variable by a different LET active.
Inform7 - an interactive fiction language - is often instructive in suggesting different approaches to syntax from those used in mainstream programming, and indeed in this case it has a couple of interesting constructions to avoid naked Booleans.
Most directly relevant to this, it has the concept of ‘options’ as arguments to ‘phrases’ (which are basically functions). This would let you write something like:
to decide what number is the calculated formula for (a: a number) and (b: a number), with gain or without gain
And within the function you could use ‘with gain’ and ‘without gain’ as if they were booleans:
If with gain, decide on a+b;
And at the calling site you would call the function like so:
Let c be the calculated formula for 3 and 4, with gain
Obviously in Inform7 you are more likely to be using this in a much more naturalistic way, effectively adding ‘adverbs’ to the ‘verbs’ your phrases are defining:
To recount the story so far, cursorily or in great detail…
Another similar Inform language feature is its mechanism for Boolean properties.
You can declare a Boolean property on a ‘kind’ or a ‘thing’ just by saying that it ‘can’ be true:
A chair can be comfy.
The table can be scratched.
You can also use ‘either/or’ forms to make these booleans richer and more expressive:
A chair can be comfy or hard.
(You can also go on and add more values, of course - at this point it’s really an enumeration)
These sorts of properties on kinds become ‘adjectives’, and you can use them in both declarations:
The overstuffed armchair is a comfy chair in the living room.
And in expressions:
If the target is a comfy chair…
The idea that Boolean parameters are really adverbs and Boolean properties are really adjectives I think says something quite profound, and there’s definitely room for other languages to do better at acknowledging the first-class role these kinds of constructs could have with the right syntax.
What about copy pasting functions and make variants? With a hint in the name about what the variant does?
Copy pasting and modifying seems like a safe low effort working solution.
Is this considered bad practice?
Yes. It's not a bad first-pass, but it can easily lead to problems if there are too many instances of it. In particular, if there is any error in the initial program that's gone undetected or any change to the requirements that it implements, then you have to fix every single copy. Good luck.
I find that many of these principles can be distilled (and replaced) by the simple adage: what is the best way to write this to make my colleague's life easier?
If someone submitted a PR where all of their boolean parameters were actually enums I would reject it, open a PR of my own from the same branch, and reject that one too.
These clever micro-optimizations are a pathology of bored, well-meaning developers.
Could you please elaborate? To me, it looks like the article posits using boolean flags instead of enums is a code legibility issue, not a performance matter. There may still be good reason to reject a large PR such as the one you described. But, I don't get where the micro-optimization appears.
[+] [-] flohofwoe|4 years ago|reply
A workaround in C99 (and more limited in C++20) is to use a single struct which bundles all the function parameters, and then use designated initialization, this also makes function paramaters optional, and at least in C99 they can appear in any order:
This is mainly useful for functions which take many parameters. One downside in C99 (but not C++) is that there's no way to define default values for struct items (other than zero/false).[+] [-] Certhas|4 years ago|reply
[+] [-] amw-zero|4 years ago|reply
[+] [-] kevin_thibedeau|4 years ago|reply
[+] [-] adzm|4 years ago|reply
https://pdimov.github.io/blog/2020/09/07/named-parameters-in...
[+] [-] cphoover|4 years ago|reply
[+] [-] mistahenry|4 years ago|reply
[+] [-] rocqua|4 years ago|reply
[+] [-] BenFrantzDale|4 years ago|reply
[+] [-] solarmist|4 years ago|reply
[+] [-] 10x-dev|4 years ago|reply
[+] [-] Sjonny|4 years ago|reply
The solution provided in the article is the way to go.
[+] [-] dhosek|4 years ago|reply
calc_formula(1,2,true)
would show as
calc_formula(a: 1, b: 2, is_gain: true)
but
calc_formula(1,2,some_var)
shows as
calc_formula(a: 1, b: 2, some_var)
Although on the flip side, it creates an incentive for the developer to choose sensible names for variables, functions etc.
[+] [-] fxtentacle|4 years ago|reply
[+] [-] cormacrelf|4 years ago|reply
> I’d like a language that can contextually resolve enums, so I can just type something like calc_formula( ia, ib, is_gain ).
Swift does this and it should be considered for any new language design. Enum.variant can be shortened to .variant, including for variants with data in them, .variant(arg). Perfect solution, because the dot is an autocomplete trigger.
[+] [-] henrikeh|4 years ago|reply
[+] [-] Joker_vD|4 years ago|reply
And in case of really large number of parameters, one would generally pass either a map or (in older code) a proplist: #{is_gain => true, other_param => 42, ...} or [{is_gain, true}, {other_param, 42}, ...]. There is no beautiful syntax for destructuring all that stuff with default values, unfortunately.
[+] [-] peoplefromibiza|4 years ago|reply
Actually there is one: records with default values.
https://erlang.org/doc/programming_examples/records.html
[+] [-] drath|4 years ago|reply
Another trick, at least in js, is to use destructuring assignment, e.g.
[+] [-] dugmartin|4 years ago|reply
[+] [-] 3jckd|4 years ago|reply
I mostly write computation/math-related code and I find using named arguments to be a good practice. This is also quite similar to OP's enum approach, e.g. sth like `calc_formula(a, b, is_gain=True)`.
To be fair, the older I get, the more I like explicit arguments for everything like in Swift (and smalltalk iirc).
[+] [-] bjourne|4 years ago|reply
[+] [-] treve|4 years ago|reply
Instead of trigonometry functions, how would you refactor JS's fetch() with many of its behaviour-altering flags?
[+] [-] wst_|4 years ago|reply
If your function returns pizza, as mentioned in other comment, adding some toppings won't change the boundaries. It still returns pizza, with peperoni or not. It doesn't change how you make the pizza. You're just adding more data to it. You may solve it with syntax, language data structures, etc. Whatever you like to make it more readable to the caller. But probably you'd want to pass toppings in arguments.
On the other hand if you have a boolean param that changes how function works. That's questionable in my opinion. Say you want to return list of users from DB but omit interns, sometimes, and you need to call API (or query DB) to know if someone's intern. You could define `omitInterns` bool argument but it seems clunky to me.
I may be mistaken, though. As said: defining boundries is not easy.
This also touches other problem a bit. Should we strive to decrease `if` branching in our functions or not? I personally tend to branch very early on, so later I can follow straight path. That's not always possible, but if it is, it helps greatly. Makes code easier to follow.
[+] [-] flohofwoe|4 years ago|reply
[+] [-] silvestrov|4 years ago|reply
now becomes 16 distinct functions.
[+] [-] magicalhippo|4 years ago|reply
[+] [-] unnouinceput|4 years ago|reply
[+] [-] CRConrad|4 years ago|reply
Citation, as they say, needed. :-)
[+] [-] dragonwriter|4 years ago|reply
With keyword arguments, this problem goes away, and not just for boolean arguments but for arguments generally.
[+] [-] btbuildem|4 years ago|reply
Trying to mitigate the boolean param dilemma, I would lean on Erlang and its multiple function signatures. It tends to force your solutions into initially harder but eventually more graceful form.
Generally, when my code starts showing these kinds of warts (silly parameters getting tagged on to function signatures), I take it as a sign that the initial tack I've taken no longer addresses the problem I'm trying to solve. More often then not it goes all the way back to an incomplete / outdated understanding of the business logic.
[+] [-] stared|4 years ago|reply
In a much simpler case of arcs in SVG, I still need to check flags to do the correct path (https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Pa...).
[+] [-] mtreis86|4 years ago|reply
One, optional and keyword arguments can have defaults.
Two, you could use a dynamic variable and a closure. Outside the body of the let the gain var returns t, within the body of the let it returns nil.[+] [-] lispm|4 years ago|reply
The function inside a LET with a dynamic variable does not create a closure. If one calls CALC-WITHOUT-GAIN later, there is no binding - unless there is another dynamic binding of that variable by a different LET active.
[+] [-] jameshart|4 years ago|reply
Most directly relevant to this, it has the concept of ‘options’ as arguments to ‘phrases’ (which are basically functions). This would let you write something like:
And within the function you could use ‘with gain’ and ‘without gain’ as if they were booleans: And at the calling site you would call the function like so: (http://inform7.com/book/WI_11_14.html)Obviously in Inform7 you are more likely to be using this in a much more naturalistic way, effectively adding ‘adverbs’ to the ‘verbs’ your phrases are defining:
Another similar Inform language feature is its mechanism for Boolean properties.You can declare a Boolean property on a ‘kind’ or a ‘thing’ just by saying that it ‘can’ be true:
You can also use ‘either/or’ forms to make these booleans richer and more expressive: (You can also go on and add more values, of course - at this point it’s really an enumeration)These sorts of properties on kinds become ‘adjectives’, and you can use them in both declarations:
And in expressions: The idea that Boolean parameters are really adverbs and Boolean properties are really adjectives I think says something quite profound, and there’s definitely room for other languages to do better at acknowledging the first-class role these kinds of constructs could have with the right syntax.[+] [-] cassepipe|4 years ago|reply
[+] [-] Jtsummers|4 years ago|reply
[+] [-] tpoacher|4 years ago|reply
[+] [-] mrslave|4 years ago|reply
[+] [-] quda|4 years ago|reply
[deleted]
[+] [-] debacle|4 years ago|reply
These clever micro-optimizations are a pathology of bored, well-meaning developers.
[+] [-] nirvdrum|4 years ago|reply