The Inconvenient Convenience of Dynamic Languages
Truthiness probably isn't worth it anymore
November 3, 2021
Truthiness is the behavior of interpreting non-boolean values as true or false in a conditional context. E.g. in this loop: while 1 { ... }
, the 1 is interpreted as true
. Dynamic programming languages provide this behavior as a convenience to save the programmer from typing more than they need to. However, this week I wasted a bunch of time tracking down a bug that had a several root causes:
- An integer stored in a database table as a float
- A database driver which returns floats as strings
- A language which treats 0, “0” and 0.00 as false but “0.00” as true
I was burned by truthiness. The trouble is, as convenient as it may be, truthiness encourages ambiguity. Look at this code, can you tell me under what conditions foo
will be true?
if (foo) { ... }
Without additional context, it’s impossible to know. This can be quite inconvenient, for example if I’m reviewing a merge request with a diff like this, I’ll have to checkout the full source to trace the origin of foo
.
Here’s a tiny table of dynamic programming languages and common truthy/falsey values:
+---------------------------------------------------------------------------+
| | Year | nil | 0 | 1 | NaN | Inf | "" | "0" | "1" | [] | {} |
|---------------|------|-----|---|---|-----|-----|----|-----|-----|----|----|
| Clojure | 2007 | N | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| Elixir¹ | 2012 | N | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| JavaScript | 1995 | N | N | Y | N | Y | N | Y | Y | Y | Y |
| Julia | 2012 | | | | | | | | | | |
| Lisp | 1958 | N | Y | Y | Y | Y | Y | Y | Y | N | Y |
| Lua 5 | 2003 | N | Y | Y | Y | Y | Y | Y | Y | | Y |
| Perl 5² | 1994 | N | N | Y | Y | Y | N | N | Y | N | N |
| PHP | 1995 | N | N | Y | Y | Y | N | N | Y | N | Y |
| Python | 1991 | N | N | Y | Y | Y | N | Y | Y | N | N |
| Ruby | 1995 | N | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| Tcl | 1988 | | N | Y | | | | N | Y | | |
| Vimscript³ | 1991 | N | N | Y | | | N | N | Y | | |
+---------------------------------------------------------------------------+
Julia doesn’t support truthiness. There is some recent convergence with Clojure, Elixir, Lua and Ruby only treating false and nil values as falsey. But overall there is no broad agreement; the programmer must keep the rules in their head when working with code.
On truthiness, Programming In Lua has this to say:
Conditionals (such as the ones in control structures) consider false and nil as false and anything else as true. Beware that, unlike some other scripting languages, Lua considers both zero and the empty string as true in conditional tests.
Beware indeed! (emphasis mine). Programming Perl takes a whole page to explain its truthiness rules⁴. The Mozilla JavaScript documentation includes thirteen different examples of truthy values.
In a popular Elm book⁵ Richard Feldman writes:
JavaScript has a concept of “truthiness,” where conditionals can be values other than true and false. Elm doesn’t have truthiness. Conditions can be either True or False, and that’s it. Life is simpler this way.
Let’s review some use cases and see if he’s right.
Checking if a value is present
Imagine that we’re writing a JavaScript program expecting a number, and want to take some action if it’s provided:
if (num) { ... }
if (typeof(num) == "number" && num > 0){ ... }
This might be Exhibit A in defense of truthiness. The truthy version is much shorter, saving type and comparison checks. But the checks are implicit, risking bugs. Is it intentionally excluding 0
? On the other hand if num
was "0"
the condition would flip to true. Whereas the non-truthy expression will only be true if num
is greater than zero.
If we only care about non-nullish⁶ values, both examples collapse to:
if (num != null) { ... }
Or if we want to be sure num
is a number:
if (typeof(num) == "number") { ... }
Safely calling a method
Truthiness can be used to avoid calling a method if the object isn’t present, by relying on &&
to short-circuit expression evaluation. Imagine if name
was optional, but an empty name was an error:
if (name && name.length == 0) { ... }
if (name != null && name.length == 0) { ... }
Here truthiness only saves 8 characters, and I’d guess it’s not faster either. But both versions are unnecessary if we use optional chaining:
if (name?.length == 0) { ... }
Assigning default values
Another common use of truthiness is to assign default values:
greeting ||= "hey"
if (greeting == null || greeting.length == 0){
greeting = "hey";
}
The truthiness version is shorter and faster. But it also risks overwriting false
, 0 and an empty string - which might mask caller issues. If greeting
was a function parameter, it could be given a default value like this:
function sendMessage(greeting = "hey") { ... }
But not all values are parameters, so what to do in the other cases? The answer is to use the logical nullish assignment⁸:
greeting ??= "hey"
Both of these solutions leave the cases of false
, 0 and the empty string explicitly unhandled.
Conclusion
Programmers are generally more productive working with dynamic languages than static ones. But the trade off of prioritizing convenience over safety isn’t always worth it. Implicit variable declaration has been a mess⁹. And whilst there are different definitions of truthiness, modern dynamic language features often provide the terseness of truthiness but with clearer, safer code.
References
- Elixir’s short circuit operators
||
,&&
and!
use truthiness but its boolean operatorsand
,or
andnot
do not - Perl doesn’t have a boolean type. To support serialization into formats which do, modules provide special objects e.g.
JSON::true
. The symbols[]
and{}
are references in Perl, which evaluate as true. But I’m using them here to refer to ordinary arrays and hashes to be consistent in comparison with the rest of the table - In Vimscript 0 is false and all other integers are true. It also has the bizarre behavior of coercing strings into integers by treating any leading digits in a string as its numerical value, else the string evaluates to 0. I.e.
:echom "10hello" + 5
equals 15`. But the real insult is Vimscript refuses to coerce floats to integers in boolean contexts 🙃 - Christiansen et al, Programming Perl 4th edition, page 32
- Feldman, Elm in Action, page 12
- JavaScript distinguishes between declared-but-unitialized variables (undefined) and the null object. Both are “nullish”
- The optional chaining operator JavaScript proposal provides a lot of background including edge cases
- Many languages support a null coalescing operator
- Nystrom, Crafting Interpreters
Tags: truthiness bugs code-review dynamic-languages javascript