top | item 28591596

Get rid of those boolean function parameters (2015)

60 points| melzarei | 4 years ago |mortoray.com | reply

114 comments

order
[+] flohofwoe|4 years ago|reply
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:

    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).
[+] Certhas|4 years ago|reply
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.
[+] amw-zero|4 years ago|reply
Many newer languages support named function arguments: Swift, Kotlin, etc.
[+] kevin_thibedeau|4 years ago|reply
For C* langs you can just insert a block comment with the arg name:

  my_func(/*a_bool_flag*/ true, yadda);
[+] cphoover|4 years ago|reply
In JS we can use JSON/object literal syntax and object destructuring to serve a similar purpose:

    myFunc({
        aBoolFlag : true,
        anotherBoolFlag : false,
        aString : "Hello World"
    });

    const myFunc = ({ aBoolFlag, anotherBoolFlag, aString }) => { /* do something with them... */ };
[+] mistahenry|4 years ago|reply
Kotlin does this, and I find it’s massively improved the readability of our codebase
[+] rocqua|4 years ago|reply
In python, I love keyword-only arguments for this. Then the caller has to write:

    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.
[+] BenFrantzDale|4 years ago|reply
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.
[+] solarmist|4 years ago|reply
I forget which version it starts in, but in newer python’s you can have keyword only arguments (they can’t be positional like in your example)

    def calc_formula(first, second, *, is_gain=False):
    …
[+] 10x-dev|4 years ago|reply
IntelliJ solves this nicely by showing the parameter name at call sites, effectively making it look like the language has named parameters.
[+] Sjonny|4 years ago|reply
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.

[+] dhosek|4 years ago|reply
Although, it doesn't always. For example,

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
Fully agree. No need to change the source code to fix what isn't broken.
[+] cormacrelf|4 years ago|reply
From the article:

> 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
Fwiw, Ada does this too.
[+] Joker_vD|4 years ago|reply
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.

[+] drath|4 years ago|reply
This should really be solved by using named parameters or by writing docblocks so that IDE can show hints.

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})
[+] dugmartin|4 years ago|reply
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.
[+] 3jckd|4 years ago|reply
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).

[+] bjourne|4 years ago|reply
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.
[+] treve|4 years ago|reply
Religiously splitting functions with boolean arguments doesn't always result in more maintainable code.

Instead of trigonometry functions, how would you refactor JS's fetch() with many of its behaviour-altering flags?

[+] wst_|4 years ago|reply
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.

[+] flohofwoe|4 years ago|reply
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.
[+] silvestrov|4 years ago|reply
function pizza(boolean pepperoni, boolean bacon, boolean mushroom, boolean artichoke)

now becomes 16 distinct functions.

[+] magicalhippo|4 years ago|reply
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.
[+] unnouinceput|4 years ago|reply
Delphi has overload and default parameters. The problem in article is non existent for a good Delphi programmer.
[+] CRConrad|4 years ago|reply
> Delphi ain't the most elegant language out there

Citation, as they say, needed. :-)

[+] dragonwriter|4 years ago|reply
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.

[+] btbuildem|4 years ago|reply
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.

[+] stared|4 years ago|reply
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).

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
There are a couple ways to approach it in Common Lisp, probably a few more than I have here.

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)
      ...))
[+] lispm|4 years ago|reply
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.

[+] jameshart|4 years ago|reply
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
(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.
[+] cassepipe|4 years ago|reply
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?
[+] Jtsummers|4 years ago|reply
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.
[+] tpoacher|4 years ago|reply
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?
[+] mrslave|4 years ago|reply
Isn't this called Boolean Blindness?
[+] debacle|4 years ago|reply
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.

[+] nirvdrum|4 years ago|reply
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.