Allow use as and try as for From and TryFrom traits

To begin with, I strongly believe that we can use as keyword for From trait. I think that usage of as keyword (that support only primitive types for now) can improve readability and reduce cognitive load.

enum State {
    Disabled,
    Enabled
}

impl From<bool> for State {
    fn from(value: bool) -> Self {
        match value {
            true => State::Enabled,
            false => State::Disabled
        }
    }
}

fn main() {
    // Some data from outside with FIXED type
    // Can be, for example, the result of some check
    let outerFactor = true;

    // Desired as much cleaner then other variants
    let someState = outerFactor as State;

    let someState: State = outerFactor.into();
    let someState = State::from(outerFactor);
}

We don't need to care about things like "can we convert f64 to f32" since as must be implemented over always valid variants (f32 to f64, CustomA to CustomB and other but may be not implemented for CustomB to CustomA - since one type can be superset of another).

Okay, what about try as?

This keyword pair can use TryFrom trait for possible conversions (such as f64 to f32 where f64 <= f32::max).

enum State {
    Disabled,
    Enabled
}

impl From<bool> for State {
    fn from(value: bool) -> Self {
        match value {
            true => State::Enabled,
            false => State::Disabled
        }
    }
}

impl TryFrom<&str> for State {
    type Error = String;
    
    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "enabled" => Ok(State::Enabled),
            "disabled" => Ok(State::Disabled),
            other => Err(format!("Unable to convert State value from '{other}'"))
        }
    }
}

fn main() -> Result<(), String> {
    // Some data from outside with FIXED type
    let outerFactor = "enabled";
    // Desired as much cleaner then other variants
    let someState = outerFactor try as State?;

    let someState: State = outerFactor.try_into()?;
    let someState = State::try_from(outerFactor)?;

    Ok(())
}

So what do you think about this?

3 Likes

That convenience comes at a cost: as would no longer be guaranteed to be a cheap conversion.

3 Likes

I'm not sure that this is critical. Since as cheapness is for primitive types, we can't say how much it will cost. Or we can say that cheap conversation is only for primitive types and cannot be guaranteed for user defined

Cheap can be partially recovered with const fn support in traits, I think

I actually want to go the other way: as has unusual syntax and currently supports a number of unchecked conversions that would be better written as checked conversions, and we’re getting closer and closer to having specific methods for everything as can currently do. I’m really tempted to ask for a lint for as to force my projects to be more specific.

Now, if as didn’t have those abilities I consider detrimental, I wouldn’t mind this as much. But if it didn’t have those abilities, it probably wouldn’t exist in the first place, and then you’d be proposing to add new operator-like syntax for a trait, and even with how common From/Into are I don’t think this would be worth it.

14 Likes

There is already a set of clippy lints to restrict as usage, the full clippy::as_conversions lint and a set of cast lints mentioned in it (some of which are even warn-by-default now).

4 Likes

What are you supposed to write when you want to do a conversion where wrapping or truncation is intentional? E.g. x as i64 when x is a u64?

I know that for now it's allowed, but in future is more desirable for me to forbit this action for truncated vales.

So in this case with new syntax is allowed to write try as or use special conversation that explicitly handle or ignore overflow case.

Maybe, this syntax can be implemented in the next edition :slightly_frowning_face: (due to backward compatible of course)

You can do this:

fn wrap_to_signed(x: u64) -> i64 {
    i64::from_le_bytes(x.to_le_bytes())
}
1 Like

The general plan is to provide methods for all these meanings. See, for example, expose_addr on pointers instead of as usize, cast_mut instead of as *mut _, ptr::from_ref instead of casting a reference with as *const, etc.

Having those exist will also help linting catch very annoying typos like i32::max_value as u32, which compiles today but does something that's almost certainly not what anyone ever wants.

8 Likes

That's great. (I was bitten by the max bug years ago.) Specifically for truncating or wrapping arithmetic (from a primitive integer to another of the same width and different signedness or to a narrower width) what are these methods going to be called?

These are such useful and common operations that they should have standard names. Writing as usize from a narrower unsigned type seems really common to me (even if there is no From conversion, but you are working on a target where there could be one). I'm happy to write a function to do it myself but maybe less happy to read an implementation of it in every codebase I encounter—or have to download a crate for such a simple operation.

The wrapping u64 -> i64 one is common? Can you provide a use case? I don't think it's very common.

For this you want a non-wrapping conversion. This is exactly why as is dangerous, since it does wrapping when what you want is not to wrap.

For non-wrapping conversions you can currently write x.try_into().unwrap().

Whenever you need to do logical right shift on an i64 or arithmetic right shift on a u64 you need to make these conversions.

For conversion to usize if I'm writing code that can only possibly work on x86-64, why should I have to write try_into().unwrap() everywhere?

The integer as-casts are not all that surprising with the understanding that

Every {integer} as {integer} cast is just the numerical value of the input, wrapped to the output type.

So if the output is iN or uN, just repeatedly add or subtract 2N until in range of that type, also known as arithmetic modulo 2N.

I find that explaining it as varying combinations of truncation or zero-/sign-extension, while relevant for the bit-representation, just makes it seem like every combination of has it's own special cased definition.

Oh, wow, and no warning. Even clippy only warns because that might wrap, but doesn't see any problem in

isize::max_value as usize
1 Like

Do you have an application in mind where you wanted either of these operations? A logical shift is normally for bit manipulation, and signed types aren't appropriate for that. Arithmetic shifts are normally for signed arithmetic, and unsigned types aren't appropriate for that.

Well because there is no better alternative, with as being risky and confusing for all the reasons people have mentioned in this thread.

If you find yourself writing conversion code "everywhere", there might be a better way to organize your code or your abstractions.

Some people have argued that 64-bit platforms should allow converting u64->usize with .into() rather than .try_into(), but that's a separate discussion.

One example found in the standard library is the implementation of f32::total_cmp. It's very much bit manipulation, that uses both an arithmetic shift (to extend the sign bit to all other bits) and then a logical shift (to zero the sign bit).

I agree the "modular arithmetic" explanation is better than what is in the docs.

However what is surprising about as for integer arithmetic is that it does modular arithmetic in the first place -- even in debug mode. "+" does that only in release mode.

OK nice. Still, that's not a common thing people want to do. Given the lengthy 22-line comment explaining the tricky bit manipulation going on in that code, it seems OK to use slightly more lengthy notation for this in code as well.

As an alternative to as, I think a good name for such conversion might be i32::to_bits(self) -> u32 and i32::from_bits(u32) -> Self, given that f32 already has those as well.

2 Likes

@tczajka @quaternic @user16251

You conversation makes me feel strong in my opinion that current as usage is very situative and UNSAFE (except type casting like Box<Struct> as Box<dyn Trait>).

In context of my propose it seems the breaking change but in the good way because all SAFE casts like i32 to u32 must and will work and all other like u32 to i32 will require refactor.


As for my DX I never use truncated casts like i32 as usize but still using u32 as usize or explicit transformation when needed.

Arithmetic right shift for unsigned values is really useful. For instance, to broadcast a bit:

fn broadcast_bit(x: u64, pos: u32) -> u64 {
    (((x << (!pos & 63)) as i64) >> 63) as u64
}

A few other applications:

  1. Incrementing a bit-reversed integer.
  2. Extracting a signed bit field from an unsigned value.

Logical right shift for signed values is a little weirder but I've used it to construct signed values from a bitstream by shifting bits in from the right.

These conversions are useful, and they already have a name in Rust. I'm just asking if we aren't supposed to write "as" what are we supposed to write in its place?

As for converting to and from usize, sometimes the API is out of your hands. For instance, file sizes and positions are measured in u64 but buffer sizes are represented with usize.

As I understand it as doesn't generate any extra code in debug mode, while a no-op try_into would.

That's the same hack as in f64::total_cmp mentioned above.

Like I said, this is extremely tricky, specialized code. As such, I don't think this is an argument in favor of a shorter notation as in exchange for less clarity and safety. This code's clarity wouldn't be hampered much by a more explicit conversion, such as with .to_le_bytes(), followed by from_le_bytes(). In fact I think the clarity would be improved by writing it out in several lines with comments about what is going on.

Another interesting trivia: if I write this code directly, without any such hackery and without any conversions, it generates better code than the version above!

pub fn broadcast_bit_better(x: u64, pos: u32) -> u64 {
    if x & 1 << pos != 0 {
        u64::MAX
    } else {
        0
    }
}