After 3 months, still learning to read Rust code

I'm going through the tokio tutorial. Very early on I encountered this line of code:

    client.set("hello", "world".into()).await?;

I'm wondering about the "world".into() part. I know "world" has the type &str. My first thought is that the str type must implement the Into trait. Maybe it turns the string into an array of bytes. To verify this I went here: str - Rust. Scrolling down the left nav I find the section that lists the traits this type implements, but Into is not there. Then I looked through the list of methods and into is not there.

I feel like I'm missing some fundamental understanding about how to read Rust code. What am I missing?

As the Into docs say,

One should avoid implementing Into and implement From instead. Implementing From automatically provides one with an implementation of Into thanks to the blanket implementation in the standard library.

So some other type has impl<'a> From<&'a str> for Something {...}.

The problem is one of code quality. Into gives the compiler plenty of information because of type inference but it doesn't give a human as much information as it could. I prefer to be as explicit as possible. To that end I never call the into method and instead make the conversion obvious to the reader. You would have to look at the function signature to know what type "world" is being converted into. Reasonable options are String, a String newtype and some specialized string types like PathBuf.

2 Likes

It seems what I failed to recognize is that I needed to start by determining the type of the second parameter of client.set which is Bytes. So the Bytes type must implement the Into or From trait. Assuming it follows the advice in the docs for Into, it would just implement the From trait. So now I'm looking at the documentation for std::str::Bytes here (std::str::Bytes - Rust) for evidence of implementing the From trait. I see From<T> listed under "Blanket Implementations", but clicking that doesn't give any indication that it is related to the str type. I just happened to notice that Bytes implements the FromStr trait. Does calling the into method on a &str use this? More than just looking for an answer to my immediate question, I'm trying to learn how I could have discovered the answer on my own by correctly interpreting the documentation.

Client::set takes a bytes::Bytes, not a std::str::Bytes.

If you click on the name Bytes in the docs, it will take you to the correct page. You don't need to search somewhere else for a type named Bytes and hope to find the right one.

1 Like

I am using VS Code with rust-analyzer. When I hover over the call to client.set I see this:


So I see the type "Bytes", but it doesn't tell me the namespace it comes from and clicking it doesn't do anything. How can I determine that it is really bytes::Bytes?

When I look at the documentation for bytes::Bytes I see this: impl From<&'static str> for Bytes which is the piece I was missing. This relies on me knowing that a call to the into method invokes this, but I guess that's just a part of Rust that beginners need to internalize.

In VSCode, you can hit F12 while the cursor is on Bytes to be taken to the definition.

1 Like

Ah, it's unfortunate that types inside the documentation pop-up are not clickable. (They are in the online docs.)

In VS Code, if you right-click on set and choose “Go to Definition” then you can click/hover on the types of its parameters to get more information.

Rust Analyzer also has an “Open Docs Under Cursor” command that opens the HTML documentation in the web browser, but there is no default key binding for it.

1 Like

In general, with certain trait methods, the type depends on what the context demands. collect is such an extreme case of this, you often can’t get away with using it without a type annotation.

One useful approach to learn what type is needed is to use an error throwing value such as () or _. For instance replace the ”hello”.into() with _. The compiler will tell you what it was expecting. Of course the function signature is another source.

In general, see the compiler errors as less of an error generating machine, but more of a “here’s what to do next” tool. This is unique because of type inference and all the engineering that went into it.

Finally, I wouldn’t say it’s “bad practice” to call into, but more so to implement it directly instead of From. There are a few traits that are the reverse of the other; where one can be inferred from the other. It’s sometimes useful to track if applying each in sequence is the same as calling id.

It’s all really well laid out in the docs.

1 Like

I tried your suggestion, replacing the line of code with this:

 client.set("hello", ()).await?;

The error message I get is "mismatched types; expected struct bytes::bytes::Bytes, found ()". As far as I can tell bytes::Bytes exists, but bytes::bytes::Bytes does not. Is this an error in the error message?

Looking at the source code, the type is actually located at bytes::bytes::Bytes, where bytes is a private module of the bytes crate; the Bytes type then is re-exported here (with pub use). Still it feels kind-of undesirable for the compiler to print out the original path of a type (from an external crate) when that path is containing private modules and the type is available (and used) through some different, public-modules-only path. Especially when there’s only a single kind-of “canonical” such public path. On the other hand, note how complex it was to even describe this undesirable setting; especially considering that it’s probably also desirable to always use the same qualified path in error messages if it’s the same type, even if it was imported through different re-exports, so I guess improving the situation might be somewhat nontrivial.


Edit: There seems to be a long-standing GitHub issue about this problem, too

2 Likes

That's not something that should be done in general, and I'm not actually sure if it's ever the right thing to do. From/Into duality is an idiom that Rust users will have to learn. OTOH implementing Into directly makes it impossible to implement From due to the blanket impl, and it in turn prevents From-based generics from working.

1 Like

I do see how this makes the code less readable than it could otherwise be. I would be happy to accept a PR that changes that line to use Bytes::from("world") instead of "world".into().

8 Likes

Ah. Great information.

I over-interpreted the following in the docs; the key miss was “prefer using... over From:

Prefer using Into over From when specifying trait bounds on a generic function to ensure that types that only implement Into can be used as well.

... the scope of the statement is “specifying trait bounds”. Not more generally based on what people are saying in this series of posts.