No matter what I do I cannot satisfy the type checker

I have following function

    pub async fn update_user<T>(
        &mut self,
        uid: impl AsRef<str>,
        fields: &[(impl AsRef<str>, &T)],
    ) -> UnitResult
    where
        T: ToSql + Sync,
    {
        let (keys, values): (Vec<_>, Vec<_>) =
            fields.iter().map(|&(k, v)| (k.as_ref(), v)).unzip();
        let query_set = keys
            .iter()
            .enumerate()
            .fold(String::new(), |qs, (i, curr)| {
                if i == 0 {
                    format!("{}=${}", curr, i + 1)
                } else {
                    format!("{}, {}=${}", qs, curr, i + 1)
                }
            });
        let query = format!(
            r#"update users set {} where "id"='{}'"#,
            query_set,
            uid.as_ref()
        );
        self.client
            .clone()
            .get()
            .await
            .map_err(str_err!(Err::ConnErr))?
            .execute(query.as_str(), values.as_slice())
            .await
            .map_err(str_err!(Err::DBErr))
            .map(|_| ())
    }

and I get the following error


Here T is both sync and ToSql so why It is complaining I dont understand the functions asks for a &ToSql+Sync and I am giving it the same thing but for some reason compiler cannot figure out that the T is the same thing
Why does this happen ?

The thing is that &T is a different type from &dyn ToSql + Sync, even if T implements those traits. You need to actually convert the references into trait objects to make your sql library happy. I can't test it as you haven't provided a playground that compiles, so could you try this out:

let (keys, values): (Vec<_>, Vec<_>) = fields.iter()
    .map(|&(k, v)| (k.as_ref(), v as &dyn ToSql + Sync))
    .unzip();

I tried that but seems it is not valid rust syntax compiler suggests first to add parentheses around and then when I add to remove them

As I said I can't test it myself. You will have to post the error message.


this way it seems to be working But I cant understand why I need to cast it and the compiler cannot figure it out ?

In the end of the day T is ToSql

Sure, T implements the trait, but &dyn Trait is a special thing called a trait object, and it is different from &T and a conversion is required to turn one into the other.

ToSql provides to_sql method which can be used as &T.to_sql as well Is there a special reason compiler ignores this?

Cant every dyn Trait method also be used with a solid type implementing that trait ?

The signature of the execute function requires an argument of type &[&(dyn ToSql + Sync)] and the type &[&T] is simply a different type. Your input must match the arguments required by the method to call it. For example:

struct A {}
struct B {}

impl A {
    pub fn foo(&self) -> usize { 0 }
}
impl B {
    pub fn foo(&self) -> usize { 0 }
}

fn takes_a(a: A) -> usize {
    a.foo()
}

You cannot provide takes_a an argument of type B. Sure, they both have a method called foo, so in some sense it makes sense, but the types are different, and they must match in order for you to call the function.

But these both are solid types in case of impl Trait if takes_a accepted a Fooer lets say both should have been able to be passed there isnt it
What is special with dyn traits then how is it different from impl trait

A trait object is not only a reference to the value, but it also has what's known as a vtable. Normally when you call a method, the compiler knows where in the assembly this method is, and hardcodes the address of the method, but when you use a trait object, this information is not known at compile time, and the vtable is used to look up where the function is at runtime.

The need to include a vtable in the reference among other things means that the reference type &(dyn ToSql + Sync) takes up 16 bytes of memory, whereas if you know the concrete type, you don't need the vtable and therefore it only takes up 8 bytes.

When you use impl Trait as an argument to a method, that is just syntax sugar for creating a generic method like this:

fn takes_fooer<T: Fooer>(fooer: &T) { ... }

When you call this function with an A, the compiler will generate a version of takes_fooer with the type A hardcoded, and if you later call it with the type B, another version of the function will exist in the binary, specialized for the specific concrete type.

On the other hand, when the function takes a trait object, no generics are involved:

fn takes_fooer(fooer: &dyn Fooer) { ... }

In this case, the function is not generic, so only a single version of this function will exist in the final assembly, and if you call it with different types, the information needed to call type-specific methods are part of the fooer reference in the vtable, and since you can look them up at runtime, you don't need to create separate versions in the assembly.

Note that this means that if the assembly was generated in a way that tried to call the dyn version without turning it into a trait object, suddenly it would be trying to go look up the information in a vtable that isn't there, and it would all break down. Now, in the takes_fooer case, the compiler will automatically insert the conversion to trait object for you, but that is not always possible to do so.

let v = vec![&1, &2];
let trait_v: &[&dyn ToSql] = &v; // error!

This tries to create a slice of trait objects, but with only an immutable reference to the vector, you can't change the ordinary references into trait objects, because that would require

  1. to change the size of the allocation since trait objects are twice the size
  2. modifying the vector through an immutable reference
  3. and modifying it back afterwards to ordinary references?

Converting every element to a trait object is not a something that can be done cheaply for a slice.

5 Likes

Oh I understand now thanks.

1 Like

I presume that the reason they want to use trait objects is that this allows you to give it a slice where not all elements are the same concrete type.

1 Like

I was gonna ask actually why cant compiler just analyze trait objects at compile time as impl Traits then I exactly noticed this :slight_smile: that we can use different concrete types in case of Trait objects
Again thanks a lot

I mean, in some cases the compiler probably could do that, but there are also limits to how far the compiler is going to optimize your code. It isn't going to remove fields from your data types, it isn't going to change your bubble sort into a quick sort, and it wont challenge the developer's decision to use trait objects unless it can inline it so much that the vtable lookup is literally right next to creating that vtable.

I wrote my function and now compiler complains that it cannot know the size of a refference at compile time how is that even possible

I'd have to see the snippet and error message to answer that.

    let fields_vec: Vec<(&str, &(dyn ToSql + Sync))> =
        [("email", fields.email), ("phone", fields.phone)]
            .iter()
            .filter(|(_, v)| v.is_some())
            .map(|&(k, v)| (k, &v.unwrap_or_default() as &(dyn ToSql + Sync)))
            .collect();
    if let Some(pass) = fields.password {
        fields_vec.push(("h", &User::hash_pass(uid, pass)))
    }
    store.clone().update_user(uid, &fields_vec);


here I am passing &ToSql which is a pointer which is supposed to be 8 bytes in my machine

No, &dyn ToSql is a fat pointer with a vtable, and it is 16 bytes. That said, it's complaining it doesn't know the size of the thing it's pointing to. You can see this because there's no ampersand in the error message. What's the signature of the update_user function?