Boolean coercion pitfalls (with examples)
Tags: languages, programming, coercion, boolean
Published: 2023-02-27 19:50:02

It's not uncommon for programming language designers to let people write conditions where the type is not boolean, by automatically coercing non-boolean, but truthy values:

```c
    ┏━━━━━━━━━ not boolean
    ▼
if (x) { 
    body(x);
}        ▲
         ┗━━━━ known to be usable here
```

This lets developers run `body` only if `x` is a kind of value that you can do things with.

It's tempting, as a language designer, when you see developers writing the same kinds of test conditions over and over to want to simplify things.

```diff
-if (pointer != null) { ... }
+if (pointer) { ... }
-if (!myList.empty) { ... }
+if (myList) { ... }
-if (result.isSuccess) { ... }
+if (result) { ... }
-if (inputText != "") { ... }
+if (inputText) { ... }
-if (count != 0) { ... }
+if (count) { ... }
```

All else being equal, a language that encourages shorter code is better.

But, the [principle of least surprise](https://en.wikipedia.org/wiki/Principle_of_least_astonishment) says we ought craft semantics that have a clear meaning, even when developers are fuzzy on the precise semantics of a phrase.

Below are examples of different ways that silent conversion, specifically of `if` and loop condition results to boolean values, can violate that principle.

----

Special "nothing here" values like `NULL` are exactly the kind of things we might want to filter out.  So maybe these two should be equivalent.

```C++
// For any pointer, p
if (p != NULL) { ... }
if (p)         { ... }

// In C++, for example, NULL is falsey, 
// and all other pointer values are truthy.
```

But it's easy to confuse containers with their content.  Developers may be surprised when the container and content coerce to different truth values.

```C++
// What if we're pointing to a boolean.
bool b = false;
bool* p = &b;

if (p) {
  std::cout <<
    "a non-null pointer to false is truthy\n";
}
```

In languages that don't have *null* pointers, *Option* and *Result* types often serve a similar function: a box of zero or one value.

```ocaml
(* This is not actually valid OCaml
 * since OCaml doesn't do coercion. 
 *
 * But imagine Some _ is truthy and
 *             None   is falsey
 *)
let x = Some(false) in
if (x) then
  ...
```

Note that Swift allows its optional types in a condition but only via a *binding condition* which makes it clear that the container is what is being checked.

```swift
// Swift
let x: Bool? = false

// When x is not `nil`, unpacks it into b
if let b = x {
  print("b is \(b)")
}

// if x { ... }   // Does not type-check
```

----

Consider programs that work with data languages like JSON and YAML that use [syntax typing](https://hitchdev.com/strictyaml/why/syntax-typing-bad/); the textual value determines its type.
For example, YAML, a common configuration language, treats many un-quoted strings as [booleans](https://yaml.org/type/bool.html):

> ```regexp
> y|Y|yes|Yes|YES|n|N|no|No|NO
|true|True|TRUE|false|False|FALSE
|on|On|ON|off|Off|OFF
> ```

Developers who work with these languages are used to thinking of words as having truth values.

There's potential for confusion when a programming language assigns different boolean valence to a string value than the language of the string's content.

This happens in many programming languages: `if (someContainer)` executes the body if `someContainer` is not empty.

For example, [Python](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) says:

> Any object can be tested for truth value, for use in an `if` or `while` condition, or &hellip;
>
> By default, an object is considered true unless &hellip; Here are most of the built-in objects considered false:
>
> - &hellip;
> - empty sequences and collections: `''`, `()`, `[]`, `{}`, `set()`, `range(0)`

So Python treats the string `''` as falsey but others are truthy, including strings like `'false'` and `'no'`.

Here's some JavaScript, which has similar string truthiness.

```js
// Unpack a value in a data language
// that the program does not control.
let request = JSON.parse(
  `
  {
    "deleteAccount": "no",
    "userConfirmationCheckbox": "off"
  }
  `
);
// But we forgot to convert those fields
// to booleans using the request language's
// conventions.
// So JavaScript's conventions prevail.
if (
    request.deleteAccount &&
    request.userConfirmationCheckbox
) {
  performIrreversibleDelete();
}
```

A programming language should support developers in checking assumptions about values that come from outside the program.  Assigning arbitrary truthiness to string values makes it harder to find and check these assumptions.

----

It's easy to confuse a zero-argument function with its result.
When first-class function values have a boolean sense, this can lead to confusion.

```python
# Python
class Animal:
    def is_goldfish_compatible(self):
        return True
class Goldfish(Animal): pass
class Dog(Animal): pass
class Cat(Animal):
    # overrides a predicate from SuperType
    def is_goldfish_compatible(self):
        return False

goldfish = Goldfish()
cat = Cat()
dog = Dog()

def play_together(a, b):
    print("%r plays with %r" % (a, b))

for animal in [dog, cat]:
    if animal.is_goldfish_compatible :
        #                           ▲
        # Pay attention here ━━━━━━━┛
        play_together(animal, goldfish)
```

It's easy to confuse the method call:

```python
animal.is_goldfish_compatible()
```
    
with a read of a [*bound* method](https://docs.python.org/3/library/stdtypes.html#methods):

```python
animal.is_goldfish_compatible
```

Especially since other classes define similarly named `is_`*\** [boolean attributes](https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv4Address.is_multicast).

----

[`Unit` and `void`](https://en.wikipedia.org/wiki/Unit_type) are common names for special values produced by functions that are meant to be called for their side effect.

They make for clearer code.  If a function's author doesn't intend to provide a value to the caller, they can simply not `return` anything.

These special values should probably not be silently treated as having a boolean valence, but in some languages they are.

This JavaScript is fine.

```js
// A lambda that returns true
let shouldDoTheVeryImportantThing =
  () => true;

if (shouldDoTheVeryImportantThing()) {
  doIt();
}
```

That lambda returns `true` when called, but maybe I'm debugging the program, so I add some logging.  I need a block to put a logging statement in, so I wrap it in `{...}`.

```js
let shouldDoTheVeryImportantThing =
  () => {
    console.log("Called should...");
    true
  };

if (shouldDoTheVeryImportantThing()) {
  doIt();
}
```

When I added a block around the lambda body, I forgot to add a `return` before the `true`. Now it returns the special *void*-like value `undefined`, which coerces to `false` in a condition.

The second version logs, but silently fails to call `doIt()`.

----

Automatic coercion results from a genuine desire by language designers to help developers craft more succinct and readable programs.

But when the semantics are not carefully tailored, this can lead to confusion.

Be especially careful around:

- generic boxes and collections-of-one that might wrap booleans, including pointer types, *Option* and *Result* types,
- [catch-all types](https://wiki.c2.com/?StringlyTyped) like `string` and `byte[]` that may layer semantics in another language which is at odds with the coerced semantics,
- producers of values like first-class function values, [thunks](https://en.wikipedia.org/wiki/Thunk), and [promises](https://en.wikipedia.org/wiki/Futures_and_promises) which might have a different boolean valence from the value they produce, and
- special placeholder values like `void` or `undefined` which, when coerced to booleans silently, mask a failure to account for a missing value.

Before designing coercion semantics, maybe ask yourself, could a proposed coercion semantics mask missing steps that the author should have performed?

Might a different mechanism (like Swift's binding condition above) suffice?

Maybe you think that the rules you've chosen are obvious and that developers will have them clearly in mind when reading & writing conditions.  "[On the arbitrariness of truthi\(ness\)](https://discourse.julialang.org/t/on-the-arbitrariness-of-truth-iness/81381)" explains how different PL designers, who probably each thought that their notions of truthiness were clear, came to very different decisions.

Thanks for reading and happy language designing.