On the arbitrariness of truth(iness)

Occasionally someone will be annoyed that Julia requires an actual true or false value in conditionals, whereas many dynamic languages (including Lisps) allow arbitrary values in conditions and have rules about what count as “truthy” or “falsey” in that context. Many static languages even allow a degree of this: C doesn’t have a boolean type and instead uses non-zero integer values to indicate truth and zero values to indicate false. Julia could easily do something along these lines by having a Base.istruthy(x) generic function that types can “register” themselves with; conditionals would implicitly call Base.istruthy(cond) to check which branch should be evaluated.

Proponents of truthiness will generally argue that it’s obvious what values are truthy and which are falsey. What’s interesting about that line of reasoning is that even though it’s supposedly obvious, different languages completely disagree on what is or isn’t truthy. Most languages with truthiness have followed C’s example and consider zero to be false and non-zero values to be true. But not all of them! Consider Clojure (I just saw this on Hacker News), which considers zero to be truthy because “0 is not ‘nothing’, it’s ‘something’”. Which is a perfectly valid line of reasoning, and highlights just how arbitrary truthiness is. Apparently Common Lisp also considers zero to be truthy, so I guess Clojure followed CL rather than C. But languages with truthiness can’t even agree on whether zero is true or false! If you think you’re safe if you just avoid weird old languages like Lisp, think again: Ruby follows Lisp here and considers zero to be true.

Even if we could all agree that zero is false (which apparently we can’t :joy:), what about other integers? In languages that copied C, they’re all truthy. But does that really make the most sense? Asking if something is true/false is generally expressed as converting a value to the boolean type, which is often viewed as a 1-bit integer. If you want to do it explicitly, you do bool(x) in Python, for example. But when you convert an integer type to a smaller integer type, you typically truncate and keep the trailing bits. By that logic shouldn’t the last bit of an integer dictate its value when converted to boolean? I.e we would consider all odd numbers true and all even numbers false? That makes at least as much sense to me as zero versus non-zero. I can imagine an alternate history where that was the common rule and everyone would be aghast if you couldn’t write if (n) {/*odd*/} else {/*even*/}. “What do you mean I have to write n % 2 in order to check for parity? It’s such a basic operation!” I suspect that our actual history only transpired because jz and jnz (jump if zero, jump if not zero) happen to have been chosen as assembly instructions. But we could just as easily have had je and jo (jump if even, jump if odd) as assembly instructions that jump based only on the last bit of a register.

Another fun thing that Lisps don’t even agree on: in Common Lisp the empty list () is false but in Scheme it is true. :grimacing:. And of course we have the same schism in more modern languages too: [] is false in Python and true in Ruby.

More fun and games with truthiness, because I just can’t stop now. Older versions of Python considered midnight and only midnight to be a false time. After a great deal of arguing and waffling, this was deemed to be enough of a footgun that it was changed in Python 3.8 (breaking change in a minor version, anyone?). But isn’t the real issue here that there’s this notion that it’s somehow better to write if t: to check if a time is not midnight than to explicitly write if t != midnight:? Doesn’t that suggest that if you want to check if a number is non-zero you should write if x != 0: rather than just if x: and if you want to check if an array is empty, you should write if len(a) > 0: rather than just if a:.

More but with strings… In Python only the empty string is false and non-empty strings are true. In Ruby all strings are true. In PHP the empty string is false and so are non-empty strings… except for the string '0'. This makes a little sense if you think that you try to parse a string as an number first and then see if that number is zero or not, but that’s not what’s happening since the strings '00' and '0.0' are both true… :joy::sob:

I could go on—I haven’t even mentioned JavaScript or Perl. But I’ll stop. You hopefully get the point: it’s almost as if these languages were just making up random shit and then claiming that it’s obvious. If you want to know if a number is zero, compare it to zero. If you want to know if a string or array are empty, check if they’re empty.

Just say no to truthiness.

76 Likes

I can do that! Some weeks ago I spent way too long on (simplified example of a more complicated real-world code)

> [42].every(() => {true})
false
3 Likes

Interestingly enough, both jz and jnz are commonly just masking out a single bit of the status register of a CPU, where the result of the previous cmp-like instruction was stored (and some other flag bits). So in a sense, if the zero flag is the least significant bit in that register, jz is exactly je of the status register!


In regards to truthiness, there’s an additional complication at play in julia. Most languages that have truthiness that I know about don’t allow you to redefine that for arbitrary stuff, it has to be supported by the runtime. In julia, allowing that to be redefined would be disastrous - you couldn’t even be sure what if a does, even if you know the type, since it may have been redefined in a different world age. Yet it would be a necessary consequence of allowing user-code to define what they consider to be truthy/falsy.

9 Likes

If someone is feeling especially impish, they don’t even need to wait for this to change in the base language.

using IRTools
istruthy(b::Bool) = b
istruthy(x) = convert(Bool, x)
istruthy(s::String) = s != ""
istruthy(t::Tuple) = length(t) != 1

IRTools.@dynamo function truthy(f, args...)
    ir = IRTools.IR(f, args...)
    ir === nothing && return
    IRTools.recurse!(ir)

    for block in IRTools.blocks(ir)
        bblock = IRTools.BasicBlock(block)
        for b in eachindex(IRTools.branches(bblock))
            branch = bblock.branches[b]
            if IRTools.isconditional(branch)
                converted = push!(block, IRTools.xcall(@__MODULE__(), :istruthy, branch.condition))
                bblock.branches[b] = IRTools.Branch(branch; condition=converted)
            end
        end
    end
    return ir
end
julia> truthy() do
           if (1,)
               println("hi")
           end
       end
hi

julia> function bar(s::String)
           while s
               s = s[1:end-1]
               @show s
           end
           s
       end
bar (generic function with 2 methods)

julia> truthy() do
           bar("Hello")
       end
s = "Hell"
s = "Hel"
s = "He"
s = "H"
s = ""
""

Credit (or blame) for this monstrosity to Phipsgabler (not sure what his discourse handle is) sloppyif.jl · GitHub

14 Likes

I see it now, hahaha. It lacks a return, so all the “returns” are undefined which is falsy…

3 Likes

I recommend people who ask me the similar thing to explain to me the following Python code:

>>> all([])
True
>>> all([[]])
False
>>> all([[[]]])
True
37 Likes

I love the fact that Julia disallows non-Bool types in conditions, it saves a lot of bugs.

38 Likes

That, or remove the braces (but in my case I had a more complicated expression so removing braces wasn’t an option) because

arrow functions do not magically guess what or when you want to “return”

Returning the last expression would be a too reasonable option, I guess.

3 Likes

4 posts were split to a new topic: Assembly: is “jump if not zero” more expressive than “jump if odd”?

Fortranners (at least this one) agree with you. Fortran has a logical type, and if you want to convert an integer to a boolean you write

tf = merge(.true., .false., i /= 0)

All these fancy-schmancy notions of “correctness”, “consistency” and “error resistance” in Julia feel so 20th century to me. Those other languages have apparently embraced postmodern relativism, that truth can be whatever feels right to you depending on your viewpoint and daily mood. That approach is clearly more inline with the Zeitgeist, as evidenced in the news every day. It’s sad to see Julia so out of touch with our times. :wink:

24 Likes

jnz/jz are very useful for pointers.

if p = function_with_optional_result(...)
    # use p, as it is non-null
end

is a common pattern in, for example, the LLVM code base.

I’m not saying this is a better convention than explicit optional types (or returning nothing that’ll throw an error if you try to use it as a pointer), but it’s still fairly convenient.

Ptr types may not be integers, but it’s fairly reasonable to have them behave the same way, and thus have truthiness of integers also be based on comparison to zero.
I don’t know the history, but this also fits the pattern of C vs Lisp having settled on different conventions of truthiness for integers.

1 Like

A post was merged into an existing topic: Assembly: is “jump if not zero” more expressive than “jump if odd”?

In the first example, Julia does the same thing, which is common in logical systems.

julia> all([])
true

all([[]]) is false because the only value is an empty list, which is falsy.

In [4]: x = []

In [5]: bool(x)
Out[5]: False

In [6]: all([x])
Out[6]: False

all([[[]]]) is too complicated to read, but breaking it up we can see the only value is a nonempty list, so it is truthy.

In [1]: x = [[]]

In [2]: bool(x)
Out[2]: True

In [3]: all([x])
Out[3]: True
3 Likes

As much as I agree with the boolean/nonboolean distinction, I often wish Julia had gone one step further and not considered booleans to be integers.

julia> supertypes(Bool)
(Bool, Integer, Real, Number, Any)

julia> true + false
1
10 Likes

I will say that adding booleans is something I’ve done quite a few times before, basically counting the number of times something happened etc. I guess one could cast the bools to int to do that but it’s a bit verbose.

4 Likes

Stick that on a t-shirt and sell it at JuliaCon.

20 Likes

Stephen Colbert is somewhere sarcastically fuming right now

6 Likes

That’s not a step further, it’s a step in an orthogonal direction.

Booleans are integers anyways, at least to same extent as Int64 and Int8 are (modular integer rings). What is gained by pretending otherwise?

3 Likes
julia> all([[]])
ERROR: TypeError: non-boolean (Vector{Any}) used in boolean context
Stacktrace:

what do you mean…