…based on a made up story:
Somewhere in the cold, dark code
>a = true && "scam"
The greedy novice wonders trough the code and asks:
>a == false
false
…so a must true. Sweet!
try
if a
println("send me a billion dollars")
end
catch e
"You got scammed!"
end
…waiting to be rich…
"You got scammed"
Whaaaat!?
I have seen that the functionality of the short circuit boolean operators have been discussed for many years (an old discussion), but given all the high-and-mighty talk about no truthinesses in Julia I was (as a newbie) surprised to find that the first statement above did not return an error…
Well, you’re abusing try/catch to “use” a non-boolean variable as if it were a boolean. Without that, you would have seen the error telling you it wasn’t a boolean in the if statement.
The following three Julia expressions are essentially equivalent:
x = a && b && c
x = a ? (b ? c : b) : a
x = if a
if b
c
else
b
end
else
a
end
In particular, notice that c is never treated as a boolean. It is simply the value of the expression if a and b are true. You can also observe this in
julia> true && 1 && 2 # 1 isn't a Bool
ERROR: TypeError: non-boolean (Int64) used in boolean context
julia> false && 1 && 2 # never tries to never evaluate 1 as a Bool
false
FWIW, julia mostly represents Bool as a byte, with 0x01 === true and 0x00 === false. However, testing for truthiness often only checks the lowest bit.
Now you might ask, what about “denatured” booleans like 0x02 or 0x03? The answer is of course “bats in your nose undefined behavior”. Sometimes such heisenbools are === true/false, sometimes they are not, depending on how the compiler felt that day. (of course behavior in interpreted and compiled mode will differ, making for a more teachable debugging experience)
Alas, you can end up with denatured heisenbools if you play funny pointer games or use Vector{T}(undef, ...).
I suspect I know know what you mean here, but just to be clear for others reading along, julia’s bools are not bytes semantically, and we do not allow truthiness:
julia> 0x01 === true
false
julia> 0x00 === false
false
julia> if 0x01
println("boo!")
end
ERROR: TypeError: non-boolean (UInt8) used in boolean context
Stacktrace:
[1] top-level scope
@ REPL[13]:1
is just an Int, but it’s not actually an Int, it’s a completely different type with arbitrarily different semantics and meaning in the language, it just happens to have a memory representation that’s identical to an Int.
No, it’s more saying that: The underlying representation is an UIn8. But there are more UInt8s (i8) than there are Bools (i1), so there are necessarily some possible memory contents that do not correspond to a valid Bool. I called them denatured heisenbools.
In julia (and rust) such denatured heisenbools are evil poison sludge: We don’t want to pay the price of treating the remaining 7 bits as structure padding, because that would make a lot of stuff more expensive (e.g. we would not be able to memcmp bool-containing structs to check for identity). So we simply assume that they are always zero and no denatured heisenbools ever appear, and try to guard the edges of our data.
If a denatured heisenbool manages to sneak in, then you are in for a bad time:
You cannot reliably check for that, because the compiler knows that the remaining bits are zero and will try its best to optimize the check away. And if you add debug print assert logging to your misbehaving code, or try to step through it in interpreted mode, then behavior will change because the optimizer takes different paths.
And you can end up in seemingly impossible situations, where something appears to be neither true nor false, or appears to be both at the same time, or change its truthiness depending on context.
I could do the same thing with my Foo struct via inner constructors and other stuff (e.g. only accept even integers or whatever).
I get your wider point here, and I get that there are real potential problems with these things, I just want to make it clear to people reading this that what you said is completely false if read literally. 0x01 === true does not hold. It’s false. They’re different types.
The reason I bring this up is that this entire thread was originally about @Magnus_Lilledahl using a try catch block to (falsely) claim that julia has truthy values when it doesn’t.
Something can only be used as a Bool if it is of type Bool. The fact that it’s possible to bypass the inner constructor of Bool and create values of type Bool that don’t conform to the actual requirements of the Bool type is of course important and dangerous, but I’d say it’s not the same thing as truthiness, and it certainly is not the same thing as 0x01 === true (which again, does not hold).
Thank you all for a very interesting and enlightening discussion, I certainly learned a lot more than I could hope for.
When I read in the manual about the how the only logical values allowed in Julia are true and false (and of type Bool), I extrapolated from this that all logical operations will only allow arguments of type Bool (which I guess is true?).
My confusion arose when I saw the following in a line of code
float(r) == p || r
where r and p are some number types. Since I (wrongly?) thought || and && were logical operators (OR and AND) I was confused about what this meant since Julia is so strict about truth values and r is a number.
But maybe it is more correct to think of || and && as conditional statements (that can be used as logical statements) as illustrated by @mikmoore?
My rather contrived story in the original post was an more an attempt to illustrate my original confusion (which was primarily about || and && and not Julias truth values) than anything else.
I see, but I think if you read back your original post, you might agree that your contrived example completely rested on try / catch throwing away an error that would have invalidated the example.
In the future, I’d maybe just say that, because this makes it much more clear what the actual problem you are addressing is.
I agree that || and && can be a bit misleading. If you want the logical operators, you should use | and &.
julia> true | 1.0
ERROR: MethodError: no method matching |(::Bool, ::Float64)
The function `|` exists, but no method is defined for this combination of argument types.
a || b should instead be understood as convenient syntax for
cond = a
if cond
cond
else
b
end
and a && b should be understood as convenient syntax for
cond = a
if cond
b
else
cond
end
this means that the left hand side of || or && has to evaluate to a Bool, but the right hand side can be anything. For the most part, these operators are used in contexts where b doesn’t actually return anything. i.e. people will write stuff like
From Julia 1.12, the docstring is quite enlightening:
help?> &&
search: && &
x && y
Short-circuiting boolean AND.
This is equivalent to x ? y : false: it returns false if x is false and the result of evaluating y if x is
true. Note that if y is an expression, it is only evaluated when x is true, which is called
"short-circuiting" behavior.
Also, y does not need to have a boolean value. This means that (condition) && (statement) can be used as
shorthand for if condition; statement; end for an arbitrary statement.
See also &, the ternary operator ? :, and the manual section on control flow.