rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.9k stars 1.56k forks source link

Allow using `as` to cast to different types (`As<T>` trait) #3592

Closed tryoxiss closed 6 months ago

tryoxiss commented 6 months ago

Thank you rust community for explaining why this is bad in a polite and respectful way. Seeing the reasons this is bad, I now fully retract this idea for any further consideration. The original post is kept for archival reasons only.


Currently, you can cast some types to other types using the as keyword. For example 100_i32 as u32 will convert it to a u32. However, this is a language builtin with no way to interact with it. I porpose a new trait: As<T> which you can implement for your types.

For example if you wanted to cast the string literal "Some String" to a string, you would need to do "Some String".to_string(), but if it had impl As<String> for &'static str you could write "Some String" as String, essentially making it a shorthand for .into(), which would be exactly the default implementation. You could also do things like "Some String Slice" as String, 12_u128 as Guid, [122, 221, 152, 1] as Ipv4Addr. (Ok this is a disorganised ramble I kept changing the paramaters and its hard to keep track of this stuff. I think its good now?)

The reason I propose a new trait, instead of adding to Into<T> is that while some things can technechally be cast, it would be a bad idea to, as not every thing that can be converted with .into(). For example, As should only be allowed with the conversion cannot fail. "String 1234" as i32 can fail because the number can be too big, or not represent a numbe. But [u8; 4] as Ipv4Addr cannot fail so it would be suitable to implement as.

Rust already has traits to interact with defualt operators, such as std::ops::Mul, and even for key parts such as functions with Fn, FnMut and FnOnce, so I don't think its that far fetched to have this too. Its cannonical home would likely be std::ops::As.

Possible signature:

// This is probably be compltely idiotically wrong I am tired
// and rust generic implementations are confusing to me right now.
// EDIT: I think this is good? Its very simillar to the Into<T> trait.

pub trait As<T>: Sized {
    fn _as(self) -> T;
}

// example implementation. Note that the `as` function is underscored due to it being a keyword it cant be used directly. If this is a half decent idea I am sure either a diffrent name can be chosen or that worked around in some way, such as demoting `as` to be a soft keyword.
impl As<Ipv4Addr> for [u8; 4] {
    fn _as(self) -> Ipv4Addr {
        return Ipv4Addr::new(self[0], self[1], self[2], self[3]);
    }
}

This may not be a good idea, so if anyone has comments, suggestions, reasons why its not good, or any other feedback please do reply with it. Just seems weird to me that nearly every feature has a core trait you can implement but as does not, and it does not seem to have a good reason for it.

### Tasks
Lokathor commented 6 months ago

Generally the language has been moving away from using as, particularly because it doesn't easily fit into the middle of a call chain like a method conversion does.

tryoxiss commented 6 months ago

Generally the language has been moving away from using as, particularly because it doesn't easily fit into the middle of a call chain like a method conversion does.

Ahh, that makes sense then. The only times I have used it are casting numbers with generics, as thw _type syntax is ugly to me (100 as u8 is cleaner than 100_u8 to me, but since types can be inferred it only comes up with things where it takes a generic number type), and started thinking it was weird it could only be used for primative numbers and nothing else.

if the language is moving away from it I support that fully, either a fully deprecated as or more functionality. Just feels like a weird middle ground currently. Thanks for the explanation!

Lokathor commented 6 months ago

Many of the conversions that used to require as are now available via method. It's not all of them, but the situation will slowly improve as time allows (there's always a million things to work on at once).

BurntSushi commented 6 months ago

I'd say we also want to move away from as casts because it is somewhat footgunn-y. The footgun aspects of numeric casts are fairly obvious (it does silent truncation), but as casts also tend to be "too powerful" in certain circumstances. For example, we recently stabilized std::ptr::from_ref as a way of getting a raw pointer from an &T, but its use is more constrained than the fully general as cast that is typically used to convert a &T to a raw pointer.

The regex crate is a good example of a library where there was a concerted effort to avoid as in as many places as possible. The specific reason for doing this is that there are many "ID types" that use u32 to save on space, but also want to index into slices. To do that, those u32 values need to be converted to a usize, and more importantly, sometimes usize values need to be converted back to u32 values. I devised a small little abstraction layer to help with that (among other things): https://github.com/rust-lang/regex/blob/a5ae35153a6ec61e64cb297155f7d91c11b629c7/regex-automata/src/util/int.rs

There are still some places where it's impossible/annoying to get rid of as AFAIK, even with the help of abstraction traits. Namely, in const contexts, calling trait methods is not allowed.

senekor commented 6 months ago

A side note about as becoming an alias for .into(): The naming conventions in the API guidelines state that conversions with "as" should generally be free, whereas "into" may have variable cost. Mapping as to .into() would dilute that naming convention, because as would generally be just as costly as .into(), causing allocations etc.

But I also agree with the bigger point that as is something to move away from. I like the clippy lint as_conversions.

Lokathor commented 6 months ago

I've never understood the "silent truncations" argument. It's not silent, it's written right on the page that a conversion happened.

BurntSushi commented 6 months ago

I've never understood the "silent truncations" argument. It's not silent, it's written right on the page that a conversion happened.

I can't tell if you're quibbling with my wording or if you're objecting to the existence of footguns with as entirely. If the former, then it seems like it's just a matter of picking different words to describe the underlying problem, of which I'd likely have no objection. But I nevertheless conceptualize the underlying problem as "silent truncation" because truncation is often, but not always, besides the point of what you're trying to accomplish by using as in the first place. But if you're objecting to the existence of a footgun in the first place, then I'm not sure what you don't understand there. We likely have a deeper philosophical disagreement that probably isn't going to be resolved here.

Lokathor commented 6 months ago

It's particularly that many people have said that exact pair of words to describe it, which I find puzzling. I guess what I would say is that if you try to shove a u64 into a u32, what else is expected other than that the value will end up in the u32 range, which is less than the u64 range. I'd agree it's a little footgunny, and I'm happy we've got a range of conversion methods, but I've never understood what else people think as should do on an out of range value than what it currently does.

BurntSushi commented 6 months ago

It's an intent-versus-behavior mismatch. Some times things are confusing because they don't behave as described (not the case here), and some times things are confusing because despite the fact that they behave as described, their behavior is still surprising. As I said above, as casts tend to couple things together that don't necessarily need to be coupled.

Fully specifying the behavior of a thing can fix some types of confusion and not other types. In this case, "silent truncation" is referring to a mismatch in expectation of behavior, where "expectation" isn't necessarily 100% well informed and correct.

what else is expected other than that the value will end up in the u32 range

It could panic. Or saturate. Or panic-in-debug-mode-and-truncate-in-release. There are lots of different possible behaviors really. I'm not suggesting it ought to do any of those things, but listing out alternative behaviors of what as could reasonably do.

tryoxiss commented 6 months ago

Thank you everyone for explaining why this is a bad idea in a polite and respectful way. Seeing this now, I am going to close the issue as I now agree that my proposal is a bad idea.