Replace Bools with Enums

A Rust enum is great at representing a fixed set of values. For instance, an enum can be used to represent the state of a connection with variants like Ready, Connecting, Connected, and Disconnected.

Even though Rust enums can be used to represent many different values, one common usage is to only have 2 different variants. Result and Option are both enums with only 2 variants.

A common data type which has only 2 states is the bool type.

Replacing bools with enums is beneficial.

Example

Original Code

fn process(is_strict: bool) {
  // ...
}

process(true)

Improved Code

enum Tolerance {
  Strict,
  NotStrict,
}

fn process(tolerance: Tolerance) {
  // ...
}

process(Tolerance::Strict)

Advantages

Easier to Read and Write

Instead of having to understand what true or false means, a well named variant can clearly document the intent.

fn process(_: bool) {
}
process(true);

When reading code like the above example, true does not give any contextual clues on what it means. For instance, it could mean to do something or to not do something.

fn process_1(is_strict: bool) {
  // ...
}

fn process_2(is_not_strict: bool) {
  // ...
}

process_1(true);
process_2(true);

Using a more descriptive enum can make the code clearer.

#![allow(unused)]
fn main() {
enum Tolerance {
  Strict,
  NotStrict,
}

fn process(tolerance: Tolerance) {
  // ...
}

process(Tolerance::Strict)
}

Boolean blindness

If there are multiple bool parameters, you may run into boolean blindness.

fn process(read_from_db: bool, is_strict: bool) {
}
process(true, false);

It is easy to mix the argument order and call the function incorrectly. Likewise, reading the code may require double-checking the function documentation.

#![allow(unused)]
fn main() {
enum Tolerance {
  Strict,
  NotStrict,
}

enum ReadFromDb {
  Allowed,
  Disallowed,
}

fn process(read_from_db: ReadFromDb, tolerance: Tolerance) {
  // ...
}

process(ReadFromDb::Allowed, Tolerance::NotStrict)
}

Using different enums, the code is easier to read compared to the multiple bools.

Furthermore, since the arguments are different types, the compiler will ensure that the intended values are passed in the correct order.

Refactoring with Types

Having explicit enum types makes refactoring easier.

If a function’s boolean argument’s meaning was changed (e.g. from is_enabled: bool to is_disabled: bool), then every call to the function would have to be checked. If an enum is used, the intent is expressed at the caller site and misinterpreted.

If the order of multiple boolean arguments is changed, then every function call site must be checked to ensure the values are passed correctly in the new order. If different enum types are used, the compiler will verify the arguments are passed correctly.

Support More than 2 Variants

If the code needs to support more than 2 variants, enums can of course support more variants. When using only bools to support more variants, the number of bool parameters increases which leads to more logic to detect correct and incorrect combinations of values.

Performant

Generally, a bool in Rust takes 1 byte. An enum with 2 variants also takes 1 byte, so there is not any performance loss by using an enum instead of a bool.

Considerations

match to exhaustively check all variants are handled

match should be the dominant way to check an enum’s value. By leaning into exhaustive pattern matching, all variants are more likely to be properly handled.

Too Many Enum Types

If there are many bool parameters, the number of enum types may become excessive.

Alternatives

Inlay Hints

IDEs can help with bool function parameters with parameter name inlay hints. The editor could display the function call like:

fn process(read_from_db: bool, is_strict: bool) {
  // ...
}

// IDE would display the "read_from_db" and "is_strict: ".

process(read_from_db: true, is_strict: false);

Inlay hints can help when reading and writing code, but there are many tools which do not support them. For instance, when reviewing code, it is likely only the raw text is visible in diffs, PRs, etc. so having more descriptive code with enums is better.

Named Function Arguments

Named function arguments are not currently supported by Rust. Named function arguments could allow more clarity at the function calling site.

In theory, they would allow something like:

fn process(read_from_db: bool, is_strict: bool) {
  // ...
}

process(read_from_db: true, is_strict: false);

// Could also allow mixing the order potentially:

process(is_strict: false, read_from_db: true);

Inlay hints from an IDE already provide some of the basic functionality, but named function arguments would allow for optional arguments and re-ordering of arguments amongst other features.

You could pass in a struct parameter to achieve some of the named argument functionality:

struct ProcessArgs {
  read_from_db: bool,
  is_strict: bool,
}

fn process(args: ProcessArgs) {
  // ...
}

process(ProcessArgs { read_from_db: true, is_strict: false });
process(ProcessArgs { is_strict: false, read_from_db: true });

Miscellaneous

Bools as Enums

There is an open issue to consider making bool a specialized enum in the Rust language.

Other Languages

Haskell, Swift, and other languages with algebraic sum types can also replace boolean type usage with their Rust enum equivalents. Most language communities recommend considering the technique for the same reasons outlined.