What is wrong with auto .into?

Just be curious, in rust, we basically do not have implicit type conversion. Why not? We already have From trait. Now when I want to convert, I need to explicitly call .into. What is wrong with calling .into Automatically? The compiler already has all the necessary knowledges regarding types.

You see, we opened a back door for Result Type. the "?" operator will silently call "From::from(xxx) " to convert your error to whatever the error in the result type you declared in the function signature.

What is the concern here not allow the complier to call .into automatically for all types with proper "From" trait implemented?

The compiler knows about the transformation (as evidenced by the suggestions you sometimes receive), but the compiler has no knowledge on the performance characteristics of a given impl From. It might require allocation, it might require invoking syscalls, it might even require IO. All of those things could cause .into() calls to be very expensive. In Rust the design philosophy is to make things as easy as possible as long as it doesn't introduce footguns. Automatic type conversion is one such footgun (for an example of this a perfomance pitfall in Chrome has been conversions between two different types of strings, back and forth, at different layers of a call stack that went undetected until profiled and that is trivially easy to reintroduce).

? is somewhat special because you have to write it. There's an implicit conversion happening, but in a place that can be seen. It would be within the design constraints of Rust to allow for example let x: String = ""~; to compile, using a similar sigil for explicit but less cumbersome .into() conversions, but it would likely go against the aesthetic sensibilities of the language.

Having said all of that, I think a long term space to explore is the introduction of an "Easy Rust", a flavor of the language that does have automatic conversions, that does use Arc in some cases instead of giving a borrow checker error. I believe that this could work in a similar way that editions do. But of course this is a bit pie in the sky, future exploration idea, not something that will necessarily happen.

8 Likes

Make sense. How about this: when we implement From trait, we use attribute or some other mechanism to indicate this implementation can be used for implicit type conversion.

Iā€™m C#, we have implicit operator, and explicit operator. It really helped a lot to keep the code clean.

Let x: String = ā€œ666ā€ is so natural but illegal in rust.

So the developers are responsible for fast implementation for implicit type conversion

Basically you are asking for implicit constructor in C++ but with default behavior reversed.

Besides previous stated reason with introducing hidden logic, this also make type inference much harder. With implicit .into everywhere, type inference with generic method is effectively broken.

P.S: Even if we allow implicit conversion, which I believe it will not happen, we still definitely not have implicit &str -> String since that requires an allocation. And we will not have integer promotion since it's such a pain in c++

7 Likes

Got it.

@zirconium-n

However for string literals it should not stop us introducing like let s: String = s"666"; right? this is already makes so many sense.

(This has nothing to do with .into so maybe off-topic)

Implicit conversions are not compatible with type inference. If you implicitly introduce into everywhere, then in many cases the types will not be inferred. ? is a good example of this: if you're using ? inside of a closure, than you must explicitly annotate the return type of that closure, otherwise the error type cannot be inferred. This is true even in the trivial case where an obvious answer would be "don't into at all and just use the current error type". For example, this fails to compile:

let closure = |x: Result<Foo, E>| {
    let y = x?;
    Ok(y.transform())
};

even though the type Result<_,E> is what is likely expected. This has been a major stumbling block for try expressions, which are otherwise a very desired feature.

Rust uses a lot of generic functions, and they just wouldn't work if into conversions were inserted everywhere. Even the simple example opt.unwrap_or_else(|| 3) wouldn't work, since the argument closure would have to be converted into something indeterminate. Even if some ambiguity resolution rules were added, it would be a major footgun, since one couldn't know which types and which impls would be used based purely on the code. One would have to run the type inference, trait resolution, and conversion resolution algorithms in the head, and they would certainly have a lot of edge cases and confusing behaviour. Choosing wrong impls and conversions can have drastically different effects.

In the languages which have implicit conversions, such as Scala or C++, they are often regarded as a misfeature. Extra compiler features, code discipline and static analysis are required to get implicit conversions under control, and they are often a cause of bugs.

Probably not, for the statement as written, but it's also not that much shorter or clearer than

let s = String::from("666");

If the type is omitted, then you would again has issues with type inference. Either because the type won't be inferred, or because it is inferred to be something undesirable.

For the specific case in your example, custom literals seem like a better solution. In fact, you have used one. This doesn't require any implicit conversions, since the literal directly constructs a value of the specified type.

5 Likes

It seems that if we have a special operator for into, which will make all of us happy. I would suggest: *, in front of an expr, it means deref, after an expr, means into, seems perfect.

I'm against this because the Hamming distance between this and other similar strings is too small. I know that the compiler can disambiguate the following, but if I'm reading the following, I will miss the space, and spend more time than it's worth wondering what the compiler is complaining about:

x* y
x * y

As it stands, it's irritating to me that * means both multiplication and dereference.

7 Likes

So what would a ** b mean? Currently it means a * (*b), but with your suggestion it could also mean (a*) * b.

Moreover * is not known to mean "convert", but only "multiplication", "dereference" and "pointer" (the last one in types only). It would be pretty confusing if it also meant "convert" in Rust.

4 Likes

How about <-

let s: String = <- "666";
a * <- b

Half joking here. I mean, I don't want any more obscure operators in the language, especially when they don't really shorten the code or otherwise improve readability that much, but if I absolutely had to replace into() with something, it would be this. Wouldn't do that though. Rust's explicitness is definitely a boon to me.

I don't think that would be an option either. a <<- b is currently interpreted as a << (-b), but with your <- operator it could also be interpreted as a < (<- b)

2 Likes

Damn! Why is it so hard to pollute the language with operators? It is almost as if the language doesn't want to be tampered with :wink:

1 Like

I agree, but I come to a different conclusion: Rust should have support for custom literals.

We already have type-inferred 1 -- it's up to inference to decide if that's an i32 or u8 or what -- so extending that to make things like let x: NonZeroU32 = 1; work sounds great.

As for why, philosophically, even safe widening conversions are bad, you want to look at combinations. Is let x: u32 = 1_u8; bad on its own? No. But how do you pick the right place and type to do something like w = x + y + z; when they all have different types? It's not arbitrary, because overflows make the different possibilities give different results.

The occasional .into() is just not that bad.

12 Likes

Hmm, Rust use to (unstably) have <- though. And it is still a token it seems. No idea how it fits into the larger grammar though. (Not that I think using it is a good idea.)

That was used for placement new.

let x = Box <- get_value()?;

Which would emplace (in C++ terms) the result.

This was an experimental feature that didn't pan out due to issues and was eventually removed.

Note that the RFC mentioned the precedence as an unresolved question.

1 Like

My expection is that s"666" should always be a String, no exceptions. Therefore no type inference is required. And in my example the type annotation is virtually & visually inserted by IDEs using rust-analyzer.

let s: String = s"666";
     --------
      ^ Inserted by IDE
let s: &'static str = &s"666";
     --------------
      ^ Inserted by IDE, however no `String` creation or heap allocations, 
        but since it is redundant, it should be given advices to remove the `&s` bit.
let s: &mut str = &mut s"666";
     ----------
     ^ An exclusive reference to a `String`, Will release when out of scope so it is not `'static`

So is it a String or not? If it is, it will allocate.

Prefix looks great but I don't think this will ever be added. However, you still can use macros:

macro s($body:tt) {
$body.to_string()
}

fn main() {
let _s: String = s!("666");
}