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.
The downside to this approach is it often doesn't survive refactors. People change the method signature, update call sites, and often ignore the comments. Named parameters avoid this downside, it's a real shame named parameters are not more common in languages.
Yep, in C mode (not C++) this has been working in MSVC already since VS2015. Clang also allowed the full C99 designated intitialization feature set in C++ long before C++20 as non-standard language extension.
I started doing this because of React but at this point ({}) is my default way of starting a function. The only thing I dislike is that it's not super ergonomic for explicit typescript declarations (but great when using typescript to check .js files).
The most annoying thing about this to me (and to be clear, I do exactly this all the time) is when you mouseover `example` at a use site, all you see is `IFunctionParameters`, not the definition of `IFunctionParameters`. At least in VS Code.
That's a good point, I've lost count of the times I've hovered my mouse over that IFunctionParameters tooltip expecting a nested tooltip to appear with the actual types
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.
The 'require named argument' solution is less strong than the enum solution. (An enum is also available in python). Indeed re-use of the plain bool in Python is less clear, as is passing it on. This makes the enum the best solution.
However, enum is also a heavy-duty solution. It requires slightly more typing, but more importantly, it requires exporting an enum to all call-sites. Both in C++ and in python this is not desirable.
I'd say the enum should be in the toolbox, especially if the flag is important to the business logic of the code, and is likely to thread throughout it. But for quick work, a key-word only argument can work just as well. Especially if the flag is never to be passed on.
It's worth noting that in Python, the enum will be slower than using a bool (how much slower? I don't know - I haven't measured it), if for no other reason than the repeated name lookups. Is it worth fretting over for something that's called occasionally? Probably not. If it's something that's going to be called a lot, e.g. in a tight loop, then it's something to be concerned about.
This is IMO one of the best things type annotations provide. Combined with Protocol you can write much better structured code with little or no runtime costs.
>... it requires exporting an enum to all call-sites. Both in C++ and in python this is not desirable.
Given the reasonable namespacing that C++ and python [modules] provide, and that you have to export the complete calling specification of the function to the caller anyway (whether it's an enum, a positional boolean or a keyword argument), what's the drawback of the enum option?
In Python, if you use ' import x from' now it becomes 'import x, y from'. Can especially make refactoring more work.
If you use namespaced imports then the call is going to become very long by having the package name included twice.
In C++ you are dealing with needing to drop the enum in a header file, requiring a two file change and making headers bigger.
The call-side has the same potential namespace problem, but less badly.
A lightweight alternative I often use is to make a named constexpr at the callsite.
`constexpr bool IS_GAIN = true;
v = calc_formula(ia, ib, IS_GAIN);`
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.
The latter is more readable, you can spot bugs easier, you don't need to remember which parameter was for visibility, and which was for indicating deletable. And it doesn't take much more to write this than a confusing boolean. It doesn't scale.
The first one will probably look confusing if you're looking at the repo through a web interface though. Or if you're looking at examples in a readme. I have personally never been limited by my "raw code writing speed", and if that was the case, I would look into touch typing/autocompletion before sacrificing readability.
Most source code will also be badly readable if you print it out ;)
So should we change our use of C++ to make it more GitHub-friendly? Or should we fix GitHub to properly work with the C++ source code that already exists?
While I think parameter names at all call sites is the right solution, I don't think it's good enough to have this in the IDE alone. I still have to review code online that says `add(a, true)`, even if IntelliJ showed me `add(a, ignore_negatives:true)`.
The point is that this is a presentation issue. There is nothing wrong with the model. It would not be impossible for the reviewing software to analyze source code and display named parameters.
That would require every bit of software that presents this program not only to understand my programming langauge, but also to have enough context on the rest of the program to actually recognize the function and know what each parameter represents.
Not to mention, code is itself a presentation layer. Why would you put some presentation concerns in one layer (e.g. identifier names, indentation&styling), but others in another presentation layer?
this is true actually. every place we read code needs to also parse the code. it’s already happening today, that’s how you get syntax highlighting. the future of all code review in the browser is a more similar experience to coding in an ide. and eventually the browser will replace the ide (and you could say this is already happening too)
OTOH, programming for the human and not for the computer, included caring about presentation. I could say there's nothing right with the model either - a non-issue, personal preference.
... An older presentation issue is the tabs versus spaces flamewar. At least that one has burnt out. Maybe because of the rise of IDE's?
That helps when you are writing the code. It's less helpful when you are modifying a function being called from all over the code base. Arguably smart enough refactoring tools will help, but smart enough compilers have been doing wonders for decades, too.
> 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.
Does this feature have a general name? Swift users seem to call it "dot syntax", which is not a good name. I want to google "C++ should have <this feature>" and find a proposal from 2013 that never moved forward, but "C++ should have dot syntax" is just silly.
Ada doesn't use the "dot syntax"; instead the enumeration literal is written as is without a dot. If I write:
type Number_System is (Bin, Dec, Oct);
type Month is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
procedure Foo (A: Number_System; B: Month);
When the type is explicit, the Ada standard calls this a "Qualified expression". But I would just say that it is a kind of type inference for enumerations.
It’s not just for enums in Swift, it’s applicable to all kinds of static functions, constants, initialisers. Has to be static (in the Swift/Java/etc sense) to work. Maybe “contextual members” or “contextual statics”. I like the latter.
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.
> 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.
Does this create garbage for the garbage collector (which might be an issue for inner loops)?
In theory yes. But for hot inner loops you will probably get it optimized away. (This is a common pattern so optimizer try to find it and undo it. Furthermore hidden classes for objects are common and when you destructure directly in the argument list escape analysis is pretty easy) It probably does hurt your performance for warm and cold code but that likely isn't too significant.
So yes, if performance is critical you should probably profile and consider avoiding this pattern, but for most cases the performance impact is very minor.
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'm not following how moving the options out of the function parameters and into the call chain makes the actual function more maintainable. It's still doing the exact same thing with the exact same options it's just pulling them from elsewhere. If anything you now have more functions to maintain on top of the function that does many different things based on the calling info.
// The original "misses the point"
trig(mode="cos", type="hyperbolic")
// The style fetch() uses today
trig({mode: "cos", type: "hyperbolic"})
// The builder refactor
trigBuilder().withMode("Cos").withType("hyperbolic").calculate()
I see the builder pattern as a way to manage lack of keyword arguments. I see very little difference between your example and the actual fetch API that takes an object as JS's version of keyword arguments.
Languages with good support for named/keyword arguments have more features such as required arguments and preventing duplicate arguments. With builder patterns your only real option is to make the builder constructor have required arguments (or throw a runtime error upon finalizing the builder).
> . I see very little difference between your example and the actual fetch API that takes an object as JS's version
true
the only difference is in the tooling
code completion for methods names works much better than autocompletion for objects' fields.
And you can't mistype a method name, it would not run and give you back a - hopefully - meaningful error, while the same is not true for objects' fields.
> code completion for methods names works much better than autocompletion for objects' fields.
That is true for vanilla JS, unknown parameters will be ignored and unset will be set to undefined. However for languages that support keyword arguments (or even TypeScript[1]) the tooling should be even better than for the keyword argument case.
Good rules of thumbs are given in the "Deciding which to use" section of the article. For the fetch() function, I'd keep most parameters as is since they don't change the essense of the function. But "cache" and "redirect" do (following redirects can cause N http requests rather than just one and using the cache perhaps 0) so I'd refactor them as new functions. Imagine adding retry functionality to the fetch() function using parameters. I think you can see how this leads to feature creep.
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.
- something is done to object when some props is set or validation, such as .withStuffedCrust("cheese"), it'll set the internal props as crust="stuffed" and stuffContent="cheese"
- it's branching. So rather than making user looking for the components or configuration themselves, library author can guide them with builder. Such as:
In this case withTrailerAttachment (and possibly withOpenBack or withBox) won't show up if you call withTwoTires(), and attach won't show up if you don't call withTrailerAttachment().
In my example the could matter, or not. I see this all the times. In your example, can you make the order matter? In my example, after each selection, you can limit or expand the further options.
Nothing to stop you from doing the same within the code of the multi-parameterized single function, is there?
if Shrimp in Fillings then begin
Add(Shrimp);
Add(ShrimpOil); // So yummy together
Fillings.Remove(Jalapenos); // Don't go together
end;
if Jalapenos in Fillings then begin
Add(Jalapenos);
end;
function pizza(boolean pepperoni, boolean bacon, boolean mushroom, boolean artichoke)
breaks down when you want to add ham, potatoes and sausages to the pizza.
Secondly, you can optimize for the common case:
fn pizza() # -> default pizza e.g. margherita
fn pizza(list_of_ingredients) # -> your custom pizza
if you we are talking of simple functions and not more complex patterns, such as piping, in Elixir I would do
pizza |> add_ham |> add_mushroom |> well_done
when using boolean parameters you are also passing down a lot of useless informations (a pizza with pepperoni would include 3 false just to remove the ingredients from the pizza and only one true) and confining yourself to a fixed set of options, that could possibly also represent an impossible state.
I love potatoes on pizza and order it at multiple pizza places. They add flavor and creaminess. The secret is to cook the potatoes properly and not just throw some french fries on the pizza.
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.
Did you just skip over the beginning of the discussion, where everyone agreed that having a bunch of anonymous booleans is bad for readability, and circle back to advocating the status quo that everyone else is trying to improve on?
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).