Use `Into<bool>` in `if` expressions, if type is not `bool`

It is possible for any type in Rust to define how they can be used in a for expression, allowing them to integrate into the language and define their behavior, if desired.

However, this is not possible for if expressions, and it’s left to the user to call the right functions, even sometimes .into::<bool>(), on the condition expression to provide the if expression with the only type supported, bool.

Since there already exists a core and native way of possibly converting any time into bool, the From/Iter traits, it is possible to improve the user experience by calling .into::<bool>() automatically, if defined, for the condition expression, whenever the type is not bool.

What do you think? Has this been considered in the language? Are there any drawbacks?

NOTE: There’s no suggestion here to implement Into<bool> for any primitive types, like i32, or other core types, like Option, here. The possible confusions that may arise from these cases is already explored and well-understood.

Do you have an example type that you would use this for?

For consistency, I would also apply this to while conditions.

That final exception isn't necessary, since every T implements From<T> and Into<T> -- bool implements Into<bool> too.

1 Like

Do have have some suggestions of the kinds of types that should use this, if it existed? It feels like anything this would be checking would be better handled with some sort of destructuring so the type system knows it was checked. (Like how if let Some(x) = y is usually better than if y.is_some().)

Some thoughts ignoring whether it ought to be done:

This may be tough to do compatibly, as it wants inference fallback (known to be troublesome) to avoid breaking currently-valid code that depends on the condition being boolean for type inference. Silly example:

if Default::default() {}

Also, I’d discourage using Into<bool>, because that means that if x and if !!x can do different things (see C++). That’s easy to fix, though, by just using Not<Output=bool> as the bound instead.

Edit: Hmm, newtyping + coherence is an interesting case.

2 Likes

Most struct Type(bool) would be a candidate here, like this Bool type that has is_true() to get a bool value: https://daseinphaos.github.io/doc/redirect/format/struct.Bool.html

I faced it implementing a Binary character property class, which is a struct Type(bool) with some helper methods: rust-unic/unic/ucd/bidi/src/bidi_mirrored.rs at master · open-i18n/rust-unic · GitHub

I believe there are many other types that have clear truthy and falsy definition and already have some kind of method that returns bool.

Although, I should note that I haven't seen much impl From<bool> for Type implemented in the wild, which I assume is because it wouldn't be that useful.

Totally! :+1:

Right!

Please see my previous comment about some cases.

I think I still see enough of is_some() in the code bases, comparing to destructuring. Although, I agree that a boolean behavior is not necessarily good for the Option type. I think I need to think more about other uses of this, besides the group I mentioned above.

This is interesting. I can't remember seeing such dependency in the code, but would love to see one!

I don't think this is a good idea. Basically you are asking for implicit coercion just for destinations where bools are required.

That type should not exist imo. Instead you should have those conversion functions as public functions which produce a bool.

Again, a bad example imo. Since the docs state

The value is true if the character is a "mirrored" character in bidirectional text, false otherwise.

Which makes me thing that the bool method should be named is_mirrored

While these types exist, implicitly converting them to bool decreases readability imo. Having an explicit is_foo method conveys the intent in a much clearer way.

6 Likes

Real example, if rng.gen() is actually a common thing to do, to randomly take a branch or not.

1 Like

Personally, I’d like it to remain as if rng.gen() instead of if &rng as the former conveys intent more clearly.

2 Likes

Example: Say we want to make a function to test if two vectors of floats are approximately equal, up to some tolerance. The signature could be

fn almost_equal(vec_1: &Vec<f32>, vec_2: &Vec<f32>, rtol: f32, atol: f32) -> bool;

where rtol and atol are relative and absolute tolerances for equality. However, it’d be nice to have some sensible defaults for rtol and atol. The way I thought to accomplish this (would love to hear of a better way though!) is to instead have the signature be

fn almost_equal(vec_1: &Vec<f32>, vec_2: &Vec<f32>) -> AlmostEqualResult;

where AlmostEqualResult is (ignoring lifetime details etc.)

struct AlmostEqualResult {
    vec_1: &Vec<f32>,
    vec_2: &Vec<f32>,
    rtol: f32,
    atol: f32,
}

and contains a function

pub fn with_rtol(self, rtol: f32) -> Self {
    Self {
        rtol: rtol,
        ..self,
    }
}

(and similarly for atol). Then it would be great to be able to implement e.g. Into<bool> for AlmostEqualResult (or perhaps a new Bool trait would be appropriate instead…) to allow things like:

if almost_equal(&vec_1, &vec_2).with_rtol(1e-3) {
    blah blah
}

Right now the signature would require something like (IIUC)

if almost_equal(&vec_1, &vec_2).with_rtol(1e-3).bool() {
    blah blah
}

which is more cumbersome/less readable IMO.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.