20
$\begingroup$

Some languages (C, C++, JavaScript, Python) allow one to use integers as booleans and vice versa:

int x;
if (x) // Equivalent to: x != 0
    y();

Or:

int x = 10 + (y > 10); // Equivalent to: y > 10 ? 11 : 10

However, other languages such as Java, C#, and Swift disallow this. They do not even allow explicit casting and force users to explicitly write != 0 and ? 1 : 0 to cast from integer to boolean or boolean to integer respectively.

What is the rationale for disallowing this? Allowing integers and booleans to freely convert allows certain logic to be expressed much more concisely, such as that to determine if exactly N conditions are true. So what are the drawbacks?

$\endgroup$
3
  • $\begingroup$ This sort of implicit conversion would probably remove the ability to do nullability checks on nullable types. In typescript it saves a lot of space when checking if something is null or undefined to have a check like 'if (!someNullableInt) ...' that checks if the value has been set. It would have to be replaced with a full check like 'if (someNullableInt === null) ...'. I think C# or VB might have the same kind of check. $\endgroup$ Commented Nov 22, 2023 at 8:02
  • 5
    $\begingroup$ Note that Python does not have bool/integer conversions. Booleans values are integers - namely 0 and 1 of an integer subtype. This is separate from general values - incidentally, including integers - having boolean meaning such as empty lists being falsy and non-empty lists being truthy. The two are separate mechanisms each with their own drawbacks. $\endgroup$ Commented Nov 22, 2023 at 13:30
  • $\begingroup$ It may not be clear which of 0 and 1 is true and which is false. E.g., the return code of a successful command or system call is traditionally 0 and that of an unsuccessful command is nonzero (with different values hinting to different reasons of failure). Nevertheless, one would rather associate true with success and false with failure. And even if one knows about this logic quirk, it is much safer to write if (something() == ERROR_SUCCESS) instead of if (!something()) $\endgroup$ Commented Nov 23, 2023 at 18:49

10 Answers 10

24
$\begingroup$

There is a particular drawback of allowing implicit conversions (aka coercions*) from bool to int in dynamic languages such as Python, where many other types have coercions to bool. Coercions are generally not transitive, meaning that e.g. allowing coercion from list to bool and coercion from bool to int, does not imply that there is a coercion from list to int, let alone that this coercion would be consistent with the composition of the other two.

So consider code like this:

def exactly_two(x, y, z):
    return x + y + z == 2

The exactly_two function uses bool-to-int coercion to test whether exactly two of the conditions are true. However, this means it relies on the arguments being bool values; yet in most places where a condition is tested, the condition is allowed to be some other type which coerces to bool. Consider:

a, b, c = [1, 2, 3], [], [4, 5]
print(exactly_two(a, b, c))

The desired behaviour of the above program is to print "True", since exactly two of the given lists are truthy (non-empty). However this program actually prints "False", since x + y + z is a sum of lists, not a sum of Booleans coerced to integers.

This can get even messier when the coercion from int to bool is also considered:

p, q, r = 2, 3, 0
print(exactly_two(p or q, p and r, q and r))

In Boolean logic, the above program should print "False" since only the condition p or q is true. However, in Python the condition 2 or 3 doesn't evaluate as True, it evaluates as 2. Then the condition x + y + z is 2 + 0 + 0, and hence the program prints "True".


*Technically, Python doesn't coerce from bool to int, rather in Python bool is a subtype of int and the Liskov substitution principle applies. But I'll say "coercion" for the sake of brevity and language-agnosticism.

$\endgroup$
0
30
$\begingroup$

The main drawback of allowing implicit boolean/integer conversions is, it makes it impossible to prevent errors that result from confusion of a boolean and an integer.

Next time you confuse the order of the arguments to a function that takes an integer and a boolean, instead of getting a nice error message that points out what you did wrong, it will compile and do something unexpected.

It is a fairly important drawback. If the conversion is infrequently used, forcing the user to be explicit is a good thing. Besides, != 0 and ? 1 : 0 are not that long to write.

$\endgroup$
12
  • 8
    $\begingroup$ You don't even need ? 1 : 0 for converting boolean to integer, you can have an explicit conversion syntax such as int(b) or b as int. $\endgroup$ Commented Nov 21, 2023 at 7:57
  • 4
    $\begingroup$ Treating ints like booleans made sense in C, which didn't originally have a separate boolean type — you always used ints, with 0 meaning false and non-zero true.  (And IIRC, when a boolean keyword was first added, it was effectively just a synonym for int anyway.)  In that case, it made sense for conditions to accept an int.  But, as you say, that has very significant drawbacks.  Strongly-typed languages with a separate boolean type don't need to treat ints as booleans — the benefits of doing so are minimal.  (As you show, it's easy to convert, though IME you hardly ever need to.) $\endgroup$
    – gidds
    Commented Nov 21, 2023 at 10:50
  • 3
    $\begingroup$ @gidds: I'm not talking about any language in particular, I'm talking about language design. When designing a language, you can introduce a more specialized syntax than ? 1 : 0 to convert from boolean to integer. $\endgroup$ Commented Nov 21, 2023 at 10:54
  • 2
    $\begingroup$ A nice Python idiom (mentioned in the link in the question) that is based on this implicit conversion is of sum(booleans) to count how many True values there are in a boolean collection. It's of course possible to also do this more explicitly with something like sum(map(int, booleans)), but I do feel something would be lost in expressiveness if this implicit conversion was absent. $\endgroup$
    – yoniLavi
    Commented Nov 21, 2023 at 13:14
  • 1
    $\begingroup$ @yoniLavi Actually in python it would probably be much safer to do sum(map(bool, booleans)) than sum(map(int, booleans)) to avoid unexpected behaviours in the case where some of the "booleans" turns out not to be of type bool. See kaya3 's answer for fun examples. $\endgroup$
    – Stef
    Commented Nov 22, 2023 at 8:53
9
$\begingroup$

Booleans may not just be 1 or 0

Much of this depends on the underlying representation of the value. My particular experience of this is with C.

Before C99, there was no boolean type, only integers treated as booleans. Boolean data of course can only be 0 or 1; but an integer type can clearly have more states. Consider the code

if (x == TRUE)
    do_something();

do_another_thing();

if (x == FALSE)
    do_something_else();

Clearly if x is boolean then one or other function should be called. But if x (as an integer) has been set to 2 for example, then neither function will be called! For that reason, coding standards often required the form

if (x)
    do_something();

do_another_thing();

if (!x)
    do_something_else();

so that all non-zero values would be covered by the first case. (I should say here that this is not just a theoretical issue. I have personally seen this bug happen, and it was really hard to find!)

C99 solved this particular problem by adding a bool type, where if (x == TRUE) succeeds for any non-zero value of x. However you can still go wrong with this. Most notably, your "efficient" method of summing bools to find how many values are set true is going to fail badly if any are stored as integer values greater than 1.

More interestingly still, you can't even quantify how badly that method will fail. C merely requires bool to have at least 2 states, and the compiler is perfectly free to store this in a single bit, a byte, or a 32-bit word. I can point you to compilers/hardware which would give you all of these cases. I've personally stumbled over a bug where two different processors accessing shared memory represented bool as a byte on one side and a 32-bit word on the other, so I actually had to replace the "better" bool definitions in the shared memory structures with fixed-size integers which both sides could agree on.

This is all somewhat specific to C, of course, but the principles apply to most languages. Ultimately your boolean value needs to be stored somewhere, and if execution speed is an issue then your language may well use a single word per boolean value so that your code runs faster. Of course this may not be as applicable to interpreted languages such as Python, where the language is already slow enough that a small extra delay is not significant.

A class of bugs can be eliminated

Bugs happen, let's be real about that. One sign of clearly-written code is that it can be easier to find bugs when they happen. And one sign of a language that helps you is where a class of bugs can be eliminated by a language feature.

If you got the wrong variable in your if (x) check (and hands up any coder who hasn't? no hands? good, we're all in the real world here) then you've got a bug. If you accidentally use the name of another boolean variable then your code will build, and it will misbehave, but at least it will misbehave in a predictable, repeatable way.

If you accidentally use the name of an integer variable though, with a language which gives you implicit conversions, all the issues above could come out of the woodwork. If your language gives you an implicit zero/non-zero comparison then at least some of those problems go away, but reproducing it is likely to be hard. If your language stops these bugs at compile time, an entire class of bugs will simply go away. This is generally a good thing (assuming you can potentially handle a bit of inefficiency in the code anyway).

$\endgroup$
8
$\begingroup$

In addition to all the other fine answers, Boolean coercions are not consistent across languages:

  • Zero (0) is considered falsy in Python and JavaScript, but truthy in Ruby.

  • The empty array ([]) is considered falsy in Python, but truthy in JavaScript and Ruby.

  • Not-a-number (NaN) is considered falsy in JavaScript, but truthy in Python and Ruby.

Note that the only two falsy values in Ruby are false and nil. That means everything else is truthy. Python and JavaScript have longer lists of falsy and truthy values. Python and C++ allow operator overloading to define custom coercions to bool.

I personally agree with the strict, explicit approach to Booleans that is enforced by Java, C#, and Rust.

$\endgroup$
2
  • 2
    $\begingroup$ Just for fun you should add POSIX shell or Bash to your list. $\endgroup$
    – detly
    Commented Nov 23, 2023 at 8:15
  • 1
    $\begingroup$ As an embedded engineer mostly working with C and C++, that's a brilliant point I hadn't even thought of. And a good prompt to me to learn some more languages. :) $\endgroup$
    – Graham
    Commented Nov 23, 2023 at 8:33
6
$\begingroup$

One particularly bad issue with int to bool conversions is that if you have an expression based language, the assignment x=7 will typically return 7. This makes if x=7 valid syntax, which is really bad since this is a common typo for if x==7.

$\endgroup$
3
  • 3
    $\begingroup$ Some people might argue that things like if (errCode = tryDoingSomething()) { handleError(errCode); } make this a good feature, and equality checks should be written flipped like if (7==x) to avoid the bug possibility. I wouldn't necessarily agree with them. $\endgroup$ Commented Nov 22, 2023 at 12:30
  • 1
    $\begingroup$ @leftaroundabout That use-case is still allowed as if((errCode = tryThing()) != 0) ..., and is more explicit that way. If I saw the code without != 0 I would have to think about it because it looks so much like a mistake. $\endgroup$
    – kaya3
    Commented Nov 22, 2023 at 21:01
  • $\begingroup$ @kaya3 I like neither of these, would prefer case tryThing() of { Ok -> ... ; Failure errCode -> ... }. $\endgroup$ Commented Nov 22, 2023 at 23:52
5
$\begingroup$

There are some common errors that result from integer to boolean conversions.

One of the most common among C beginners is the strcmp() function. As a comparison function, it's easy to assume that it returns a boolean, with the truthy value meaning the strings are equal. But it's actually tri-valued: negative and positive for unequal (indicating whether one string is lexicographically lower or higher), and zero (C's falsey value) meaning equal.

Another class of functions with a similar problem is functions that search a string or array and return the index of a match. Since most languages use zero-based indexes, 0 is a valid index of a successful search, and some other value must be returned to indicate failure. JavaScript returns -1; PHP returns false (you must use the strict comparison operator to avoid automatic coercion that makes 0 == false); Python returns -1 from some functions and raises an exception from others. But again, it's a common mistake to treat the searching function as boolean.

$\endgroup$
1
$\begingroup$

There might not be a sensible, unambiguously correct conversion

The popular Numpy third-party library for Python provides a clear case study for this possibility. Because Python is dynamically typed, it is forced to accept code that would invoke an implicit conversion. Early on, the language implementers chose to allow such conversions to be attempted, and specified that types could define this conversion using the __bool__ "magic method".

This is a trade-off made for pragmatic reasons; in many other places, Python refuses implicit conversions and remains fairly strongly typed (for example, it does not allow concatenating an integer to a string with the semantics of implicitly using a string representation of the integer). In the case of conditional logic, the Python implementers apparently felt that requiring conversions all the time to avoid exceptions would be too tedious. However, the Numpy implementers found a need to refuse this convenience.

Numpy defines an array type that provides a bunch of operator overloads that implicitly "broadcast" operations - comparing two identically-shaped arrays element-wise, or repeating the comparison along one or more axes if the array shapes are "compatible".

In particular, for two arrays a and b of the same shape, the == operator (via overloaded __eq__ method) will compare each pair of corresponding elements, and return a new array of booleans of the same shape with the comparison results in the corresponding places:

>>> import numpy as np
>>> a = np.ones((3,3))
>>> b = np.ones((3,3))
>>> a[:,1] = 0
>>> b[1,:] = 0
>>> a
array([[1., 0., 1.],
       [1., 0., 1.],
       [1., 0., 1.]])
>>> b
array([[1., 1., 1.],
       [0., 0., 0.],
       [1., 1., 1.]])
>>> a == b
array([[ True, False,  True],
       [False,  True, False],
       [ True, False,  True]])

This sort of functionality is considered very useful, and a huge part of the reason why Python enjoys the popularity it does today with data scientists.

So the next question is, what should we expect to happen for if a == b:, or in a conditional expression like 'y' if a == b else 'n'?

Python has built-in fallback logic for comparisons - normally, everything that isn't explicitly falsy via an overloaded __bool__, is truthy (via object.__bool__). However, Numpy explicitly overloads __bool__ for arrays to raise an exception unless the array has exactly one element. (It used to consider zero-element arrays falsy, but current versions also raise an exception here.)

Why?

This is in accordance with the Zen of Python: "In the face of ambiguity, refuse the temptation to guess".

Think about it: if a == b: comparing two Numpy arrays, presumably, probably meant to check whether every corresponding pair of elements is equal. But the result of a == b is just an array of boolean values. Meanwhile, if c:, where c was an already existing array of boolean values, probably means to check whether any element is True. There is no way for the __bool__ operator overload to know which was meant, because it does not have access to the AST.

Moreover, such logic is often wrong-headed anyway. Commonly, people want the code inside the block to describe work to do with a single pair of elements, and the code author wants that calculation to be broadcast - but Numpy simply cannot work this way. There definitely isn't any operator to overload, because if is a statement rather than an expression. But even the conditional expression form doesn't work, and Numpy couldn't make it work if they wanted to. One might think that 'y' if a == b else 'n' ought to produce an array of strings, but even if it could work, this would be deeply magical and many Pythonistas would complain about it being complex and surprising. (Even using the x if y else z conditional expression is not universally accepted; but it would be especially hard to accept that the result might be neither x nor z!)

Instead, that job requires a different tool, such as numpy.where, or using the a == b result as a mask for another array, etc.

See also the corresponding canonical on Stack Overflow about the error message generated by the attempted implicit conversion - especially my answer there.

$\endgroup$
0
$\begingroup$

Many of the advantages associated with allowing implicit conversion could be achieved by instead providing convenient means of handling the scenarios where such conversions would be most useful. If one wants to branch if foo and bar have any bits in common, being able to write an expression where foo and bar are the only things that look like values, e.g. if (foo & bar) is nicer than having to do something like if ((foo & bar) != 0). If, however, there were an operator which would accept two integers and return a Boolean, that would make intent even clearer than the form using implicit integer-to-Boolean conversion.

Additionally, if a language had an operator that behaves like Javascript's && operator, yielding the second operand's value when the first is "truthy", then an expression like (a==b) | ((x==y)*2) could be written as ((a==b) && 1) + ((x==y) && 2). I'm not sure && would be the best choice for that operator, but having an operator with such semantics would accommodate the second most common use case for implicit Boolean/integer conversions.

An as-yet unmentioned downside to opening up arbitrary integer-to-Boolean conversions in languages which support bit fields, the semantics of Boolean bitfields would differ from those of other types. For example, given:

struct foo {unsigned char bit:1; _Bool boo:1;} it;
void test(int value)
{
  it.bit=value;
  it.boo = value;
}

invoking test(2) would set it.bit to zero but it.boo to 1.

$\endgroup$
9
  • $\begingroup$ I'm not sure how your proposed && operator works when the first operand is false. Presumably it needs to evaluate to 0? $\endgroup$
    – kaya3
    Commented Nov 21, 2023 at 21:35
  • $\begingroup$ @kaya3: It would evaluate to a zero of the second operand's type. Shorthand for x ? y : 0. $\endgroup$
    – supercat
    Commented Nov 21, 2023 at 21:37
  • $\begingroup$ I don't see what's wrong with if ((foo & bar) != 0). Very few extra characters in exchange for substantially better understandable code, without needing any special definitions. In fact I'd be tempted to write it like if ((foo & bar) != 0x0000). $\endgroup$ Commented Nov 22, 2023 at 12:38
  • $\begingroup$ @leftaroundabout: In many cases, the intended semantics would check whether some subset of the bits in a value match a pattern--equivalent to if (((value ^ pattern) & mask) != 0), but if the pattern bits are all zero, ^pattern can be omitted. This construct only works if the right hand side of the != is zero, but writing out the 0 in source code suggests that some other value could be substituted. $\endgroup$
    – supercat
    Commented Nov 22, 2023 at 16:00
  • 1
    $\begingroup$ Well, conceptually what we're dealing with here are sets, then the special case is just an element lookup if (fnorble∈control), or if (widget.control.contains(FNORBLE)), and the general case is if (isEmpty(foo∩bar)) or if (foo.intersect(bar).empty()). Of course a typical set data type is much slower than bitmasks, but you could have a type parameterised by a type-level list that has an interface like a set but is under the hood just an integer where each bit denotes an element. It's not too difficult to code this up in Haskell, and should also be possible in modern C++. $\endgroup$ Commented Nov 23, 2023 at 0:18
-1
$\begingroup$

If int implicitly converts to boolean I can't write x ?: 3 where x is maybe<int>, and ?: is null coalesce operator. This ends up being ambiguous between coalesce null to 3 and coalesce 0 to 3. (GCC has the latter operator: 0 ?: 3 does in fact produce 3. The operator is more useful when the left side is a pointer.)

If you make the choice maybe<int> -> bool, you then have to choose between 0 is kept or coalesced. If you choose coalesced, your type system ultimately requires backtracking to determine the type of a chained coalesce (maybe<int> ?: a : b is hard to find the type of if b is a ternary). If you choose kept, you then have to decide what maybe<int>(0) ? a : b does. If you choose a, you end up with a big difference between maybe<int> and int such that the program becomes hard to read if you have the var/auto mechanism for getting type of variable from its right side (which is typically a function call return); and if you choose b, you have a behavior difference between coalesce and ternary.

If T[] (array) collapses to bool; it's ambiguous between maybe<[]> and {} (empty array); empty array collapsing to bool as false always bugged me for this very reason, but collapsing to bool as true is totally unexpected. (Yes I really did mean to write collapse as this converts a vector to a scalar.)

You really don't want maybe<bool> converting to bool; bug rats nest. If you think you need it, consider allowing if(maybe<bool>) to be valid as-is; that is if, while, and for take maybe<bool> directly. This will, however, make for a small headache if you also have maybe<T> converting to bool as the standard way to check if a maybe has a value, which will typically exist in languages that do implicit conversions to bool.

$\endgroup$
3
  • $\begingroup$ I'm having trouble following the reasoning here. In languages I am familiar with, if we have something like maybe<int> then a null coalesce operator will be sensitive to a None/Null/maybe:None value that is separate from boolean false. Can you clarify why you assume conversion int -> bool also implies conversion maybe<int> -> bool? Are you confusing conversion to bool with conversion to null/undef/...? Perhaps clarifying by which language's semantics you are reasoning here would be helpful for making the answer easier to follow. $\endgroup$ Commented Nov 23, 2023 at 16:04
  • $\begingroup$ @MisterMiyagi: I filled in a whole bunch of reasoning. Ultimately this is hard to understand because it's a case of something has to be surprising but the language designer can choose which thing is surprising. $\endgroup$
    – Joshua
    Commented Nov 23, 2023 at 16:46
  • 1
    $\begingroup$ Swift had a similar problem. They allowed optionals in an if, treating nil as false and not nil as true. Then someone realised that an optional bool that is false behaved differently from a non-optional bool that is false. That feature was removed. $\endgroup$
    – gnasher729
    Commented Dec 3, 2023 at 18:35
-2
$\begingroup$

The reason to not allow it is that booleans are not numbers. Just like pointers are neither numbers nor booleans. So without 50 years of prejudice from C programming, there is no sane reason why a cast, either implicit or explicit should be allowed.

$\endgroup$
2
  • 3
    $\begingroup$ The question asks what the drawbacks are; you have not proposed any drawbacks. This answer is really just a philosophical opinion. $\endgroup$
    – kaya3
    Commented Dec 3, 2023 at 18:45
  • $\begingroup$ The obvious drawback is confusion between two incompatible concepts. $\endgroup$
    – gnasher729
    Commented Dec 7, 2023 at 9:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .