A few trivial beginner questions

  1. Why does this compile without error
fn get() -> Option<&'static str> {
    let s = "hello";
    Some(&s) // type: Option<&&str>, should fail with type mismatch
}

...and why does this complain?

fn get() -> Option<&'static str> {
    let s = "hello";
    let o = Some(&s);
    o // ...and here it fails with an actual type mismatch
}

 
 

  1. Why is this valid:
use std::collections::HashMap;
fn main() {
    let dict: HashMap<&str, i32> = vec![
        ("Norway", 100),
    ].into_iter().collect();

    let _ = dict.get("Norway"); // Shouldn't the param be of type &&str?
}

...and this is not valid?

use std::collections::HashMap;
fn main() {
    let dict: HashMap<i32, i32> = vec![
        (50, 100),
    ].into_iter().collect();

    let _ = dict.get(50); // Must be &i32, unlike the case of string slice
}

Coercions.

Coercions only apply when there is no type
inference, so &&T will coerce to &T if there is no type inference, and let o = introduced the type inference which disallows this coercion.

HashMap::get requires a reference, and string literals are &str

You can do

let _ = dict.get(&50);
3 Likes
  1. &foo in Rust is a bit magical, and it tries to do what you mean, through Deref trait and coercions. When Rust knows what type you want through type inference (that is direct enough), then it will make & give you the right type. That's how &vec gives &[] when it needs to, instead of &Vec.
fn ge1t() -> Option<&'static str> {
    let s = "hello";
    let o: Option<&'static str> = Some(&s); // specify wanted type at time of dereference
    o
}
2 Likes

There's something more going on here: accessing an element of the hash map doesn't require the exact key type, only that the provided type borrows to the key type. This is the signature of HashMap::get():

pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> where
    K: Borrow<Q>,
    Q: Hash + Eq, 

Note how k is not of type &K but an arbitrary type &Q where there's an equivalence between K and Q except borrowing.

And since T implements Borrow<T>, consequently &str implements Borrow<&str>, so providing an &str as the argument to get() when the key type is also &str does work.

The reason for all this roundabout logic is that this allows you to put owning keys into the map but look them up using their borrowed counterpart, i.e. this is an optimization that allows lookup without having to allocate. Example:

let mut map: HashMap<String, i32> = HashMap::new();
map.insert(String::from("owned string"), 42);
let value = map.get("borrowed string");

This compiles, although the type of get is &str not the owned String type, and you didn't have to allocate another owned string just for the sake of reading from the map.

4 Likes

Thanks everyone! All the replies are very helpful.

On where type coercion is performed, I found a related chapter in the reference: Type coercions - The Rust Reference, Seems like I need to give it a good read.

Thanks for the detailed explanation! Now I see that according to trait.Borrow.html

Types express that they can be borrowed as some type T by implementing Borrow<T>

Which means that the somewhat vague description "borrowed form of the map's key type" actually has a clear definition:

If type T implements Borrow<B>, then type B is a borrowed form of type T.

This is certainly very intense for a beginner!

However for me there is still some confusion.

  1. According to borrow.rs.html#213-217, there is a blanket impl Borrow<T> for T, which means such a type T can be borrowed as itself.But obviously I cannot use type i32 in a HashMap<i32, _>::get()

    (The link in your post is actually impl Borrow<T> for &T)

  2. The above deduced definition of borrowed form seems like kind of "inversed", In the example of HashMap::get(), the trait bound K: Borrow<Q> is actually putting a restriction on Q, not K, (Q must be a borrowed form of K) which is kind of confusing.

    As a result I cannot think of a way to use it as a trait bound in a function signature.

    For example, how to specify a generic parameter T which must be a borrowed form of i32? I imagine something like this:

    fn test<T>(x: T)
        where i32: Borrow<T> {} // i32 can be borrowed as T
    

    Obviously this is not valid.

I'm afraid I'm not quite following. Those are synonyms, or, rather, the same word used in two slightly different phrases. What's the confusing part about that?

Why could you? The get() function takes a reference, of type &Q. And i32 is not a reference. The fact that the trait bounds would allow i32 if the key type was plain Q doesn't somehow magically override or neuter the other constraints imposed by the function signature. If the function expects a reference, you can't pass it a type that is not a reference, no matter what.

I wouldn't say it "puts a restriction on Q, not K". It puts a restriction on both, or on their relation, if you will. Keep in mind that type checking is actually solving a constraint satisfaction problem. It's like it doesn't matter if you say y = 2x or x = y / 2 when solving an equation, the result and the relation of the two variables is the same. This equation doesn't put a constraint or X or Y, it constrains how they behave with respect to the other one. You can freely choose one or another, but not both.

It is valid, although it isn't very useful for a primitive type. Its usefulness mostly shows itself when dealing with containers and wrappers.

2 Likes

Huge thanks for the follow-up explanation!

I think my confusion comes from not being familiar with trait bound syntax and taking guessed meanings for granted.

For example, I think it's relatively easy for a beginner to think "If &i32 is the borrowed form of i32, then the borrowed form of &str must be &&str."

Also, the fact that trait bound is a restriction on all mentioned types, not only the type to the left of the colon, is not immediately obvious. It only became obvious to me after digging from the reference and found the related description in generics.html#where-clauses, it states: "...as well as a way to specify bounds on types that aren't type parameters. Bounds that don't use the item's parameters ... are checked when the item is defined."

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