Get rid of those boolean function parameters (2015)(mortoray.com) |
Get rid of those boolean function parameters (2015)(mortoray.com) |
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:
my_func((my_struct_t){
.a_bool_flag = true,
.another_bool_flag = false,
.a_string = "Hello World"
});
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). 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. my_func(/*a_bool_flag*/ true, yadda);https://pdimov.github.io/blog/2020/09/07/named-parameters-in...
myFunc({
aBoolFlag : true,
anotherBoolFlag : false,
aString : "Hello World"
});
const myFunc = ({ aBoolFlag, anotherBoolFlag, aString }) => { /* do something with them... */ }; v = calc_formula(ia, ib, is_gain=true)
You also have the option of defining a default value for the argument so the old call-sites don't even need modification.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.
enum gain_type { enable_gain, disable_gain }; def calc_formula(first, second, *, is_gain=False):
…The solution provided in the article is the way to go.
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.
Choose:
AddElement(object, true, false);
AddElement(object, true, true);
AddElement(object, false, false);
AddElement(object, false, true);
or AddElement(object, visible::on, deletable::off);
AddElement(object, visible::on, deletable::on);
AddElement(object, visible::off, deletable::off);
AddElement(object, visible::off, deletable::on);
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.> 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.
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.
Actually there is one: records with default values.
Another trick, at least in js, is to use destructuring assignment, e.g.
function calc_formula({a, b, is_gain}){
...
}
calc_formula({a:1, b:2, is_gain:true})Does this create garbage for the garbage collector (which might be an issue for inner loops)?
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).
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.Citation, as they say, needed. :-)
Not knocking it, after all it's my daily driver. You can do quite a lot with it these days and it can be quite productive.
With keyword arguments, this problem goes away, and not just for boolean arguments but for arguments generally.
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.
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...).
One, optional and keyword arguments can have defaults.
(defun calc-formula (ia ib &optional (gain t))
...)
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. (defvar *gain-enabled* t)
(defun calc-with-gain (ia ib)
...)
(let ((*gain-enabled* nil))
(defun calc-without-gain (ia ib)
...))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.
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
(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:
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.These clever micro-optimizations are a pathology of bored, well-meaning developers.
Instead of trigonometry functions, how would you refactor JS's fetch() with many of its behaviour-altering flags?
as @flavius29663 said (https://news.ycombinator.com/item?id=28593669) you can use the builder pattern
FetchBuilder()
.withUrl(ur)
.withMode("cors")
.withCache(true)
.withHeader('Content-Type', 'application/json')
.accept('*/*')
.post()
.then(response => response.json())
.then(data => console.log(data));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.
now becomes 16 distinct functions.
pizza_with_pepperoni_and_bacon_and_mushroom()
Wow glad I took out those boolean params!or any combination of the above: CreatePizza() .WithMussroom() .Build()
Even better, you can add new ingredients without changing any of the existing signatures: CreatePizza() .WithProsciuto() .WithTomatoSauce() .Build()
First of all, this
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. function getPizza(toppings: Iterable<Topping>): Pizza {
...
}Totally agree this is better to be in the language proper so we don't need this extra tooling.
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);
Then this is a valid call: Foo( Dec, Dec )
https://godbolt.org/z/eP5qMj3K1When 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.
https://www.adaic.com/resources/add_content/standards/05aarm...
https://docs.swift.org/swift-book/ReferenceManual/Expression...
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.
calc(x, y, mode="gain")
where the function is defined as def calc(
x: float,
y: float,
*,
mode: Literal["gain", "render"],
) -> float:
...
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.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?
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.
But the 2nd one will always be more verbose, no matter if you need it or not.
So I'd choose the first one.
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?
createPizza({bacon: true, artichoke: true});
This has the same benefit you describe of being able to add parameters without altering any old call sites
createPizza({prosiuto: true, tomatoSauce: true});
- 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:
myVehicleBuilder.withSixTires().withTrailerAttachment().attach(container)
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(). 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;
etc. No?In the image: pizza with potatoes, a typical roman recipe
https://i0.wp.com/www.puntarellarossa.it/wp/wp-content/uploa...
You might as well put some mashed potatoes in your rice and some pasta in a sandwich while you're at it.
It's like someone said "what type of carbs would you like with this meal" and the answer was "yes".
// 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()the difference, IMO, is - in Javascript - that this
// The style fetch() uses today
trig({mode: "cos", typ: "hyperbolic"})
^^^
would fail silently while // The builder refactor
trig().mode("cos").typ("hyperbolic")(val)
would trigger a compilation errorbut, IMO, passing objects is good enough most of the times, and I consider it a much better solution over passing boolean flags
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).
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.
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.
[1] https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABMO...
interface IFunctionParameters {
userId: string;
name: string;
age: number;
}
const example = ({userId, name, age}: IFunctionParameters) => {...}1) Too many languages don't have native enumeration and set types to make a clean and elegant solution even possible.
2) Too many developers don't know how to do it, even if they use a language where it would be possible; some because they couldn't figure it out, but most probably just out of rote habit.
And yeah, sure, not quite everyone. But most, AFAICS. And I haven't seen any coherent argument against it from anyone actively claiming it's bad (Idunno, are there any besides you?), so feel free to try again and contribute something more convincing than your previous attempt.
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?
You can either change your program to fit existing tools, or you can build smarter tools. I prefer the latter.
> code itself is a presentation layer
Not for the tool it isn't
edit: I think we can all agree that ideally we fix this in the language itself by adding optional named parameters
... 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?
My guess would be not IDEs but autoformatting tools, especially when communties like Go all follow one style.
I think the point was that it shouldn't be.
> There is nothing wrong with the model
Seems to me there is.
In your case you would end up calling your function like this: DoToppings(true, false, true, true);
Whereas in my case you would call the builder directly with (only) the params you want different than the defaults.
You could use named arguments, but that doesn't solve the problem completely. You will still have a large method signature, hard to use, harder to refactor, harder to test, error prone.
It also causes a lot of redundant code: the called usually only cares about one or 2 arguments, it's rare that you *need* to pass more. The rest of the args are filled in by defaults in the implementation. There could be elegant ways to handle the defaults, like overloading methods or just passing in defaults in the method signature. Default value are pretty bad IMO, for all the reasons above. Btw, if you *need* to pass that many arguments to a function, that is another code smell worth it's own discussion.
Multiple overloaded methods could have about the same amount of code int he implementation like the builder pattern, but they have a huge drawback: the caller cannot mix and match which arguments they want to pass in. If you have 4 arguments, there would be quite a high combination of parameters (18 possibilities? - 18 functions); Using the builder pattern you have to implement 4 methods only, and you're covering all the possible combinations the client might want. You can also limit some combinations in elegant ways right in the IDE while the developer is writing the code.
Think of FluentAssertions https://fluentassertions.com/introduction They have literally hundreds of possible assertions that are represented by object instances. You can combine them in an almost infinite number of possibilities.
Sure, there is no black and white, and depends on the language, the builder pattern is a good tool to have in the toolbox.
No, I think you're misunderstanding. Weird... Aha: My fault, sorry.
> In your case you would end up calling your function like this: DoToppings(true, false, true, true);
What?!? Heck no, that wasn't what I meant, why would you think that? [Goes repeatedly clicking "parent"] Aha, I see: Sorry, the threads and sub-threads have branched so I got confused as to where we are.
No, that wasn't what I meant at all. I got this sub-thread mixed up with sibling ones, and was talking in the context of languages with native enums and sets (roughly, Pascal and its descendants), where you do:
type PizzaFilling: (tuna, shrimp, peperoni, ham, gorgonzola, jalapeno) ; // Etc, etc...
PizzaFillings: set of PizzaFilling ;
function MakePizza(PizzaFillings);
And then the body of function MakePizza uses that set as per my GP comment.It's called not with a bunch of anonymous booleans like you wrote, but with a set of enumerated descriptively-named fillings as a parameter; say, a TakeOrder function builds this set by starting from an empty one (or perhaps tomato and cheese already in as defaults?) by the customer's specifications, and then calls
MakePizza(OrderedFillings);
> Sure, there is no black and white, and depends on the language,Yeah, I was attempting to show how the problem you mentioned doesn't exist in languages with better / saner types. Again, sorry for getting the contexts mixed up; I though that was what you were talking about too, and just didn't get.
> the builder pattern is a good tool to have in the toolbox.
Urgh, yeah, I suppose so... At least in languages where you need it, because they lack other more basic (Heh!) amenities.
function MakePizza(Fillings: PizzaFillings);
of course.Sorry, but why should people care about what sounds terrible to you?
> put some mashed potatoes in your rice and some pasta in a sandwich while you're at it
If you weren't too obsessed with yourself, you'll know that that pasta actually exists, it's called "pasta e patate" and someone has put it in a sandwich for sure...
there is also a very popular variant made of pasta, potatoes and mussels.
> It's like someone said "what type of carbs would you like with this meal" and the answer was "yes".
It's like someone asked you "what are you first World problems" and your answer was "yes"
The recipe I'm talking about come from Italian rural tradition, when people were poor and carbs were the only thing they could afford to eat to keep being alive, not a privileged people's self inflicted fictional problem.
Sorry for the brutal honesty.
But you anglophones are not qualified to judge other culture's food. Your food is usually terrible.
I never asked you to care what sounds terrible to me.
Opinions are like assholes. Everyone has one, most are full of shit, and I don't really care if you don't like mine.
If you supposedly don't care what I think, why bother trying to tell me my opinion is wrong? You can't have it both ways.
> you weren't too obsessed with yourself, you'll know that that pasta actually exists
Ok sure buddy. It's totally on me that a meal that may consist of entirely carbs is not common/popular outside of Italy. Totally my fault.
> It's like someone asked you "what are you first World problems" and your answer was "yes"
Eating a diet of just carbs is literally not a first world problem, it's a 3rd world problem because people can't afford (or can't adequately store) proteins, fresh vegetables etc.
> when people were poor and carbs were the only thing they could afford to eat to keep being alive, not a privileged people's self inflicted fictional problem.
People have eaten much worse sounding things than your double-carb special, no doubt. The difference is - you're bollocking on like it's a perfect meal, and the idea that it doesn't sound appealing is apparently insulting to you.
Who could have possibly ever foreseen that a dish made out of necessity because people literally had nothing more than two kinds of otherwise bland starchy carbohydrates, would not seem appealing when other options are available?
An opinion isn't brutal honesty bub. You have one, I have one. The difference is I am well aware that mine is an opinion.
Good job on the generalisations though. "the food of half a billion people is usually terrible, here come try some carbs on carbs.".
Simple. Because you started it.
> Ok sure buddy. It's totally on me that a meal that may consist of entirely carbs is not common/popular outside of Italy. Totally my fault
Exactly it's totally your fault for being ignorant.
Pasta/rice with potatoes it's common in many "not born yesterday" cultures in places like China, India, Africa, not exactly a small percentage of the World population.
> would not seem appealing when other options are available?
You're still talking out of ignorance.
There were a lot of other options. Meat, for example, was common back then and of higher quality than today, but meat was sold for money, because rich people loved it.
> , it's a 3rd world problem because people can't afford (or can't adequately store) proteins, fresh vegetables
complaining about carbs it's a fictional problem white privileged people invented to feel special.
So, yes, it's a first World problem.
Also: I've said poor, not 3rd World, which, BTW, has been changed to developing countries.
The fact that American pilgrims, that were poor, haven't developed a balanced diet and starved to death or due to nutritional deficiencies, says a lot about the terrible diet they had back home and nothing about other cultures that did, at the same time, being equally poor.
Don't try put words in my mouth, please.
> you're bollocking on like it's a perfect meal
See?
You can't handle the truth.
I've only said you don't know what you're talking about.
Never said anything about the quality.
I, for example, don't eat pasta with potatoes but I do eat pizza and potatoes.
And am not a "monster".
> The difference is I am well aware that mine is an opinion
But you aren't aware that your opinion is also wrong, so technically you are making a mistake, I told you it, but don't wanna learn.
Tell me you come from a British colony without telling me.
> "the food of half a billion people is usually terrible, here come try some carbs on carbs."
Yes, exactly.
It is so terrible that they usually eat other culture's food.
You don't regularly eat haggis, Which is, BTW, sheep inside sheep, or shepherd’s pies, fish and chips, bangers and mash ..., do you?
Overall this concept of copy/pasting with context is nice, but it only works for applications that have a defined API between them, such as MS Office OLE objects. In all other cases, pasting plain text is better than any "smart" solution.