DEV Community

Cover image for No love for boolean parameters
Dominik D
Dominik D

Posted on • Originally published at tkdodo.eu

No love for boolean parameters

Ah, booleans. 0 or 1, true or false. Always either one of them, never something in between. So simple and predictable. At the end of the day, all code we write winds up in lots of zeros and ones.

There is nothing wrong with booleans per se. We use them for conditions every day:

// ✅ boolean condition
if (user.age() < legalAge) {
    return 'Sorry, you are too young to use this service'
}
Enter fullscreen mode Exit fullscreen mode

But using them for parameters to a function can be an indication of bad design for various reasons:

Single responsibility principle

A function should do one thing, and one thing only. Passing a "flag" to it often indicates that a function does two things at once, thus violating this principle. For example:

// 🚨 booleans as a function parameter
function createReport(user: User, sendEmail: boolean) {
    // create the report here
    const reportData = ...

    if (sendEmail) {
        sendReport(user.email, reportData)
    }
    return reportData
}
Enter fullscreen mode Exit fullscreen mode

There seem to be some cases where we want to send the report as email directly after creating it, and some cases where we don't. But why entangle this in the createReport function? That function should only create the report, and nothing else. The caller can decide what they want to do with it.

They are confusing

Flags can be confusing, especially in languages where you don't have named parameters. Take for example this signature of equals from the Kotlin standard library:

fun String?.equals(other: String?, ignoreCase: Boolean): Boolean

// Returns true if this string is equal to other,
// optionally ignoring character case.
Enter fullscreen mode Exit fullscreen mode

As opposed to the first example, the function doesn't do two things at once, it does one thing in two different variations - an important difference. This can be highly confusing when you have to read the call-side that looks something like this:

"foo".equals("bar", true)
"foo".equals("bar", false)
Enter fullscreen mode Exit fullscreen mode

How should we know what true means in this context. Even worse, what would false mean? Does it maybe negate the equals comparison? Scala has solved this differently with two methods: equals and equalsIgnoreCase. Each does one only one thing - no guesswork here.

More guesswork

Before you look it up here - what do you think this boolean flag on Groovy's List.sort method means:

["hello","hi","hey"].sort(false) { it.length() }
Enter fullscreen mode Exit fullscreen mode

In case it isn't obvious to everyone:

  • mutate - false will always cause a new list to be created, true will mutate lists in place

Totally logical and intuitive api, not confusing at all 🤷‍♂️

Impossible states

Booleans make it easy to create impossible states. Suppose you have a metric of some sorts, and you want to format that. It might be a "normal" number, but it might also be a percentage value. So you decide to model the formatting function like this:

function formatMetric(value: number, isPercent: boolean): string {
    if (isPercent) {
        return `${value * 100}%`
    }
    return String(metric)
}
Enter fullscreen mode Exit fullscreen mode

This is rather rudimentary number formatting function, but apart from that, it doesn't look too bad. Frankly, the first "flag" you add to a function usually looks very innocent.

The second flag

Requirements change over time (as they tend to do), and now we have to support currencies for some of our metrics as well. Starting from the above formatting function, we are tempted to add another flag, isCurrency

function formatMetric(value: number, isPercent: boolean, isCurrency: boolean): string {
    if (isPercent) {
        return `${value * 100}%`
    }
    if (isCurrency) {
        return // imagine some currency formatting is returned here
    }
    return String(metric)
}
Enter fullscreen mode Exit fullscreen mode

Our code works, we write tests, add the currency flag if we have a currency metric, and all is well.

Except it isn't.

Adding one boolean doesn't add one more state - the amount of states grow exponentially. Two booleans means four states, three booleans means eight possible states etc. What happens if we call our above function with:

formatMetric(100, true, true)
Enter fullscreen mode Exit fullscreen mode

The answer is: you can't know. It's an implementation detail which flag is checked first. It's also an impossible state: A metric cannot be percent and currency at the same time. Such impossible states are frequently introduced with boolean parameters. I recently encountered a function with 8 booleans as input - turns out, it only had 3 actual states, the rest were variations thereof.

Resist the urge

To avoid impossible states, resist the urge of adding the first boolean parameter. It is infinitely easier for humans to extend existing patterns instead of recognizing anti-patterns and refactoring them. If there is one boolean, there will be a second. If we start of with an enumeration of possible states, it is much more likely that this will be extended instead:

function formatMetric(value: number, variant?: 'percent'): string {
    if (variant === 'percent') {
        return `${value * 100}%`
    }
    return String(metric)
}
Enter fullscreen mode Exit fullscreen mode

Now we can extend the variant to 'percent' | 'currency', and only have three states to work with instead of four. Of course, you can also explicitly include the default (standard) variant instead of using undefined.

Moar advantages

Further advantages of a single variant property include:

  • Better type safety
    We've already covered readability, but it's also very easy to mix flags up, and because they have the same type (boolean), the compiler won't tell you about it. You can work around this by using a single options object, which is quite popular in JavaScript.

  • Exhaustive matching
    I've written about exhaustive matching in TypeScript before, and it also comes in very handy in this example. The compiler will then tell us where we need to adapt our code when we add a new variant. CDD, compiler-driven-development:

type MetricVariant = 'standard' | 'percent' | 'currency'
function formatMetric(value: number, variant: MetricVariant = 'standard'): string {
    switch (variant) {
        case 'percent':
            return `${value * 100}%`
        case 'currency':
            return // imagine some currency formatting is returned here
        case 'standard':
            return String(metric)
    }
}
Enter fullscreen mode Exit fullscreen mode

We also do the same when creating React components, or have you seen a Button with an isPrimary and and isSecondary flag? Of course not - because how can they be both at the same time?

// 🚨 Don't do this
<Button isPrimary isSecondary />

// ✅ Do this
<Button variant="primary" />
Enter fullscreen mode Exit fullscreen mode

The wrong abstraction

Oftentimes, flags are added because we see similarities to existing code, and we don't want to repeat ourselves, keeping everything DRY.

  • Here is a function that looks almost like what I want, I just need to add this one flag, because it's slightly different.
  • This component looks like I could use it for my case as well, I just need a withPadding property added so that it fits.

There is lots of good literature available on that topic, showing why we shouldn't do this and what we could do instead:

I can recommend them all, and for starters, resist the urge of adding the next boolean parameter to your codebase.


No matter if you like booleans, or not, or both at the same time, leave a comment below ⬇️

Top comments (11)

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️ • Edited

I find that quite often flags are added to toggle code at the end or at the beginning of a subroutine. For a simple example:

function equal_strings(a, b, ignore_case)
   if ignore_case then
      a, b, = a:lower(), b:lower()
   end
   return a == b
end
Enter fullscreen mode Exit fullscreen mode

In these cases, the refactoring is quite easy: extract the common functionality into another subroutine:

function equal_strings(a, b)
   return a == b
end
function equal_strings_ignore(a, b)
   a, b, = a:lower(), b:lower()
   return equal_strings(a, b) -- EDIT: Forgot to change this line after copy-paste 😜
end
Enter fullscreen mode Exit fullscreen mode

When the code being toggled happens in the middle of the subroutine, it's often the kind of situation where passing a function as an argument is a better option:

function equal_strings(a, b, normalize)
   if normalize then
      a, b = normalize(a), normalize(b)
   end
   return a == b
end

equal_strings("foo", "Foo", string.lower)
Enter fullscreen mode Exit fullscreen mode

Cases where neither of those apply, are often a sign of over-complicated program logic, or code that shouldn't be shared at all.


Note: examples in Lua because that's what I find easy to write.

Collapse
 
patricktingen profile image
Patrick Tingen • Edited

But now you have the logic in two places, shouldn't you call one function from the other to avoid that?


function equal_strings(a, b)
   return a == b
end
function equal_strings_ignore(a, b)
   a, b, = a:lower(), b:lower()
   return equal_strings(a, b)
end
Enter fullscreen mode Exit fullscreen mode
Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Indeed, that was what I intended to do, but apparently I forgot to actually change that line after copy-pasting it. You're totally right in that this only makes sense when you actually use the subroutine with the extracted logic :D

Collapse
 
tkdodo profile image
Dominik D • Edited

yes, passing the function is what is described in the inversion of control article I linked to. Definitely worth a read, a very nice concept.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️ • Edited

Calling it "inversion of control" almost seems a bit weird to me because it implies that it is the "inverted" form of how it should normally be. But the example of a select (or filter or whatever one wants to call it) function makes it very obvious how it's really just another way to write a loop. It's not "inverted"; it's just normal control-flow.

Collapse
 
tkdodo profile image
Dominik D

Definitely! I kinda omitted the fact that there are legit use-cases 😅, like if it's a boolean in a real-world scenario (enabled / disabled is a good example). I was more focussed on putting attention on wrong usages of booleans, because I think they outweigh the "real" booleans by a lot :)

see also the comment from @mcsee below: dev.to/mcsee/comment/1c1h8

Collapse
 
baenencalin profile image
Calin Baenen

I would say flags don't always have to be part of SRP as long as the "responsibility" is broad enough to fit it. Technically that function could have a single responsibility.
Like class C and class D extending from class B which extends A.

Collapse
 
mcsee profile image
Maxi Contieri

in such cases you can use polymorphic decorators

It is a balance between hated booleans and over design

if you need to put just one IF, I guess booleans would be ok. if you have several ones you should favor using polymorphism as stated in Dominik's article

Collapse
 
tilkinsc profile image
Cody Tilkins

A jumble of good points you have here.

I, myself, didn't realize my inner hate for Java's String stdlib was due to booleans. Reading Java code just simply put me off and I had no idea why.

Another point in case in C and C++: your system almost always allocates a WORD (32bits on 32bit system, 64bits on a 64bit system) for creating a variable on the stack. However, did you know that with the stdlib, the range of said bool is from 0-255? That's right - it's a byte. In a typical program you are likely to allocate a lot of bools. This is a lot of wasted space in my opinion. GCC will optimize a lot of situations like if statements down to a cmov instruction. However, if you call a function that isn't a leaf function const char* formatMetric(int64_t value, bool isPercent); then you bet its just a lot of wasted stack space. What's even worse, is you get a LOT of padding situations in structures.

In a library I've developed and not released called CPO (a CLI application paradigm), you are rest assured that I use bitpacking on an int64_t (64bit systems) to ensure the density of the sheer amounts of booleans that appear in CLI applications.

Great article!

Collapse
 
mcsee profile image
Maxi Contieri
Collapse
 
tkdodo profile image
Dominik D

very nice, ♥️ your series. Also very good concept of exceptions for real world scenarios.