Awhile ago, I recall expressing to @bradcray that I was surprised Chapel allowed integers to be used in statements that expect a boolean.
I might be misremembering, but I believe some code in the IO module was what triggered me to first bring it up. The code looked roughly like this:
// The compiler will treat the C enum `syserr` like an integer.
var err: syserr = ENOERR;
//
// Some IO code follows...
//
if !err then respondToError();
Other expressions that evaluate to an integer are also valid, in other conditional constructs:
while 5 + 3 do writeln("Hey, world!");
Classes can also be used in place of booleans, in a more limited fashion...
class SomeClass { var x: int = 0; }
var x: owned SomeClass?;
if x then x = new SomeClass();
assert(x == nil);
Changing the expression in the if statement results in a compiler error:
class SomeClass { var x: int = 0; }
var x: owned SomeClass?;
if !x then x = new SomeClass();
assert(x != nil);
Test.chpl:5: error: unresolved call '!(owned SomeClass?)'
$CHPL_HOME/modules/internal/CPtr.chpl:399: note: this candidate did not match: !(x: c_ptr)
So classes are a bit inconsistent in that respect.
This is a very C-like behavior. I believe that most modern languages have tended towards prohibiting non-boolean types from being used in conditional constructs.
As I see it, the main arguments _against_ this behavior are:
As I see it, the main arguments _for_ this behavior are:
Let's see what a few other languages have to say...
In my original argument against this behavior I brought up Python, certain that it was evidence in favor of my case. It seems I've misremembered (or rather, had been _taught_ to always use a comparison operator, because I had no clue you could even do these things!):
while 1: print('Hello!')
x = 0
if not x: print('World!')
while (3 + 5): print('Maybe?')
All three of these statements will fire.
I won't try to argue that Java holds much sway over the future design of Chapel. Still, it does illustrate my point that not all "modern" languages permit integer types in conditional expressions:
public class Foo {
public static void main(String[] args) {
int x = 0;
if (!x) {
System.out.println("Hello!");
}
}
}
Main.java:5: error: bad operand type int for unary operator '!'
if (!x) {
public class Foo {
public static void main(String[] args) {
while (5 + 3) {
System.out.println("Possibly?");
}
}
}
Main.java:4: error: incompatible types: int cannot be converted to boolean
while (5 + 3) {
Rust is a big contender here, as Chapel has taken quite a bit of inspiration from it, and it happens to be the most modern language on this list. Survey says...
fn main() {
while 5 + 3 {
println!("Hello?");
}
}
error[E0308]: mismatched types
--> code.tio:2:8
|
4 | while 5 + 3 {
| ^^^^^ expected bool, found integer
|
= note: expected type `bool`
found type `{integer}`
fn main() {
let mut x = 0;
if !x {
println!("Maybe?");
}
}
error[E0308]: mismatched types
--> code.tio:4:5
|
4 | if !x {
| ^^ expected bool, found integer
|
= note: expected type `bool`
found type `{integer}`
error: aborting due to previous error
Personally, I _love_ C, and there was a time where I relied on this behavior a lot when writing C code. However in the past couple years I've sought to make my C code more principled. I've started explicitly writing out my comparisons in the hope that they are easier to follow and prevent me from accidentally introducing a bug.
Chapel has the luxury of being able to adopt a more modern, strongly typed approach rather than force programmers to rely upon style conventions.
I would also be curious to know how much this behavior is relied upon in existing Chapel code. As I see it, one of the main obstacles here is determining how much code will break if such changes are implemented.
I could be wrong, but think that determining how much code would break might be quite easy: Remove all non-bool _cond_test() overloads that accept non-bools from ChapelBase.chpl.
Remove all non-bool _cond_test() overloads that accept non-bools from ChapelBase.chpl.
I gave this a quick try, but quickly ran into problems: I believe due to compiler-generated code relying on this behavior (specifically for initializers). In the time I spent on it, I wasn't able to track down where the code was being inserted in order to see how hard it would be to add a !=nil to it.
Thanks for giving this a try. I can dig into the compiler when I've got some spare cycles try and find out where it is. Since you mentioned initializers, perhaps @benharsh would know?
I'm not sure off the top of my head. I'd just debug the compiler to find the problematic AST and debug again to find when it's created.
Trying something lazy: If I disable the int cases but keep the class cases, I only had to change one internal/standard module to get the hellos to compile. I consider that mildly promising. Trying a full test run on that version prior to digging further into the class case, just to see how bad it is and how sad I'll be to rewrite cases that relied on if myInt.
(edit: 892 failures in full comm=none testing... I haven't looked yet to see whether a small number of modules are causing a large number of failures).
So it sounds like it's fair to say that _quite_ a bit of module code relies on this idiom...
Not necessarily... One problem in IO.chpl that's used by 892 could cause all of the problems. I addressed the first tier of int conditionals in module code in about 15 minutes and am running a new test to see what happens.
Down to 217 failures after resolving that first tier of module errors. And there are still lots of errors stemming from modules (that had probably been masked by the previous ones).
161 failures after fixing the second tier of module errors, all of which look to be in the tests themselves.
So does that mean it's fair to say that there are _161_ tests that rely on this behavior? I'm not sure how I feel about that number...
Right, though they aren't all unique tests. E.g., some cases are 10 variations on a given benchmark or exercise. And of course, this is still only the ones that rely on if int comparisons. I haven't yet disabled if class cases.
I think that if x then should work for bool and also for nilable classes (indicating not nil).
It definitely shouldn't work for real, imag, or complex.
It doesn't bother me personally if it works for int and uint.
I think we should also consider if if myString then should be the same as if myString != "" then. I don't know if that's the case now, but many scripting languages do this.
While the thought of if x then for nilable classes seems appealing, if we are going to make an exception for classes instead of writing if x != nil then we might as well make principled exceptions for as many primitive types as we can (such as string, as Michael brings up), in a way that makes sense for each specific type.
Stylistically, I'm starting to think that Chapel should lean towards the scripting language flavor instead of Rust.
Though if we do allow types other types besides bool in conditional expressions, I hope we will also provide user types with the ability to implement such behavior themselves (I'm not sure what this would look like, an operator or a special method?).
My two cents: I agree with Michael's comment above. The string case worth considering, but I am a bit skeptical about that.
I think having nilable class support is valuable. Having support for integers maybe open to abuse and can lead to reduced readability in some (many?) cases. But personally I am fine if the compiler doesn't do the policing there.
Thumbs up if you're OK with leaving conditionals as they are today, capable of accepting expressions of a variety of types such as int or non-nilable classes.
Thumbs up if you think it's important to restrict conditional constructs to expressions with bool type only.
I voted for leaving them as-is because I heard enough diversity of user and developer opinions who liked at least one of the non-boolean options that I think it's better to keep supporting them for convenience. For those who prefer a stricter definition of bool tests, we could look into adding style-checker flags that would warn about cases which were not strictly bool in user code.
Thumbs up if you're OK with leaving conditionals as they are today, capable of accepting expressions of a variety of types such as int or non-nilable classes.
I voted in favor of leaving as is, but I also favor extending support to the other cases mentioned by @mppf:
I think that if x then should work for bool and also for nilable classes (indicating not nil).
I think we should also consider if if myString then should be the same as if myString != "" then. I don't know if that's the case now, but many scripting languages do this.
I believe these cases can be added later on as non-breaking changes, so we can safely defer those decisions for now.
Another idea to consider in the future is to allow user-defined types to define this behavior, similar to Python's __bool__ method.
It definitely shouldn't work for real, imag, or complex.
Why not?
I voted for bool only. From an ease-of-use (really, ease-of-remembering-the-language) point of view I think the only two options that make sense are bool only, or _any_ type. Anything in between invites questions such as "If this type is allowed, why not that type?", or "Why is it like that language, instead of this one?" for languages that only allow a subset of their types to be used in this way. It definitely doesn't make sense to me that the integral subset of the numeric types would be legal as a conditional and the real subset would not. Nor do I don't think C's treatment of integral 0 as a boolean false is necessarily a good thing to follow. C treats int as boolean simply because the language didn't originally even have booleans. int was all there was. And 0 is the distinguished value for C conditionals only because in the ISA of the Digital Equipment Corp. systems C was designed on/for it was the only value upon which you could base a conditional branch without first doing some sort of comparison instruction. Following other languages' practices where they provide sound leadership is a good thing for Chapel to do, but C's treatment of int as boolean doesn't fall into that category for me.
It definitely shouldn't work for real, imag, or complex.
Why not?
It's just that comparisons of floating point numbers are usually not one actually wants in a real application so it might be better for programmers to see == 0.0 to identify the issue.
@gbtitus
From an ease-of-use (really, ease-of-remembering-the-language) point of view I think the only two options that make sense are bool only, or any type.
Are you talking only about primitive types here? Or are you saying that you should be able to do if myList and if myRecord e.g.?
... the only two options that make sense are bool only, or any type.
Are you talking only about primitive types here?
No, all types. It should be possible to create composition rules that say how to reduce any non-primitive type to a single conditional value.
That doesn't make sense to be because not all types will have a default value; in other words there will be types that don't have a "zero". Consider for example a non-nilable class type.
Another idea to consider in the future is to allow user-defined types to define this behavior, similar to Python's __bool__ method.
I believe a sufficiently motivated user could do this today using undocumented features, but agree that we could consider making this a more official language feature.
With respect to Greg's comments, I tend to think that permitting any type to be able to opt into such a feature feels more important than having every type work in a conditional by default with no effort (and worry that such support would be more confusing than helpful).
That doesn't make sense to be because not all types will have a default value; in other words there will be types that don't have a "zero".
This doesn't carry much weight for me because the choice to make 0 equivalent to false long ago in C was, as far as I know, arbitrary. It did have to be 0 because of the ISA consideration I cited, but the meaning of 0 could just as well have been true.
The current situation seems to be that we'll allow conditionals to be formed from any primitive type that includes some sort of distinguished value, and for each such type we'll treat that special value as "false". That seems pretty ad-hoc to me, even if it does follow the lead of some other languages.
Consider for example a non-nilable class type.
Sure, the values of such a type wouldn't include "false". And if Chapel had Ada-style numeric range types, a range type that didn't include 0 wouldn't have a "false" either. That's a natural consequence of relying on distinguished values.
Edit: I should clarify that by "Ada-style numeric range types" here I mean numeric types such as int and real but which are limited to specific ranges of values. They're related to Chapel ranges but not exactly the same. However, thinking about this aspect of it has led me to: a Chapel range that doesn't include 0 wouldn't have a "false" either.
Greg: To make sure I'm understanding, are you suggesting that if valueWithoutObviousZero should just always evalute to the true fork? (e.g., if 1..10 then would be true?)
are you suggesting that
if valueWithoutObviousZeroshould just always evalute to the true fork? (e.g.,if 1..10 thenwould be true?)
Range values are actually a problem for my earlier statement that conditional statements ought to be able to take values of any type. When I said that I was thinking of array and record types, where it's easy to imagine that if any scalar type can be the subject of a conditional then one could compose the rules for those scalar types to form rules for arrays of such types and records containing fields of such types. But I don't see a natural way to derive a conditional true/false from a range value in a reasonable way, so I'm forced to conclude that not all Chapel types can be allowed in conditionals after all. (The situation would be different for Ada range types, because those are just numeric types with a low and/or high bound, whose variables hold values of that numeric type guaranteed not be be beyond the limit(s).)
I think we could still say that any primitive or synchronization or class type is allowed in conditionals and that tuples, arrays, and records are allowed iff their elements and fields (respectively) are of allowed types. But not ranges or domains, because I don't see a reasonable way to define what values of a range or domain type would be false or true.
Regarding arrays specifically (from @gbtitus 's line of reasoning above) one interesting outcome of allowing if myArray would be an arguably satisfying composition with issue #11490. From issue #11490, the current decision is that A == B produces an array (or promoted expression?).
var A = [1, 2];
var B = [1, 2];
if A == B {
}
That means the code above won't compile. But, if we allow if myBoolArray it will work.
Whether this is great ("Look at this simple pattern that now works as expected!") or horrible ("Now I will be even more confused about == on arrays...") depends on your perspective.
I think it's intriguing, anyway.
I'm feeling pretty strongly that we shouldn't do more with this issue at present w.r.t. the current release. I.e., I don't think we should scale conditionals back to bools only because I don't think there's sufficiently broad support for it in the user or developer camps. And I don't think we should look at expanding the set of expressions that conditionals currently do support because there hasn't been sufficient time to review, implement, and live with those decisions (and, they arguably aren't breaking changes).
FWIW, to me, the most natural interpretation of if range or if array or if domain given the rest of the language would be to promote the conditional / while loop / etc., assuming that the array's eltType or domain's idxType was a legal type for a conditional. But I remember we had challenges relating to doing so in Chapel's early days. I can't recall offhand whether that was because of deep semantic challenges or just being in the early stages of the implementation.
It seems like we've collectively decided that we're more or less OK with the current state of affairs. I'll leave this thread open till the end of the week. Thanks everybody!
I think it's reasonable to leave it open for the time being until we've had time to consider whether we want to expand boolean support to other types. (i.e., my previous comment was only meant to be w.r.t. the current release, but we can continue to wrestle with the question).
Personally, I think such a discussion (i.e., "should user defined types be able to be used in conditional expressions?", etc) warrants a new thread instead, but sure I'll leave this one open.
I agree that that discussion could be forked off into its own thread, though I interpret the main one going on here as being less about user-defined types and more about collections of types. It could also be forked off until its own issue, but until they are, I think keeping this one open as a placeholder makes sense (given that GitHub makes it incrementally harder to find closed issues).
Sounds good to me!
Most helpful comment
Thumbs up if you're OK with leaving conditionals as they are today, capable of accepting expressions of a variety of types such as
intor non-nilable classes.