Explanation for this code please

Hi,

Just a quick question,

// trait
pub trait Serialize {
        type Input;
}

// implementation for tuple
impl<T0, T1> Serialize for (T0, T1)
where
        T0: Serialize<Input = <T0 as Archive>::Archived> {}

For this example imagine tuple is (u32,u32).
I implemented the primitives and their Input has already been defined.

I don't really get why I need to further constrain Serialize with <Input = <T0 as Archive>::Archived> since the compiler could find out by itself.
There is a bit I miss here (because otherwise it would work that way).

Could someone explain me ?
Is it because the compiler only reads the function declaration to make sure everything is ok (hence we need to give it as much info as possible in the declaration).
It helps with the compilation performance but it implies the dev writes more. Am I right ?
I need to understand the logic to comply with it.

Thanks

What makes you think that the compiler can figure out Input = <T0 as Archive>::Archived? You haven't really provided any information about this Archive trait, so I can't tell where that information should come from.

Can you share a complete example?

Sure, let me create a simplified playground

Are basically you asking why you need

impl<A, B> Serialize for (A, B)
where
    A: Serialize<Nominative=A> + Debug,
    B: Serialize<Nominative=B> + Debug,
    //           ^^^^^^^^^^^^

in this thread?

It's because the implementation has to work for all types A and B that meet the bound -- including all possible future implementations, not just the current ones. So if you need to rely on some sort of type equality, you need to put it into the bound.

E.g. in that other thread, there's nothing preventing an implementation of Serialize where the associated Nominative type is not Self.


If you want some implementations for a fixed set of types, and want to rely on details of those implementations that aren't expressible as trait bounds,[1] sometimes macros to create a bunch of implementations on the specific types are a better approach than generic implementations.


  1. or are just annoying to express that way ↩︎

Here is the playground. Careful, it's simplified to only show what bothers me. The Serialize trait is much fatter than this. and its serialize_value has been truncated of many of its parameters that are non essential to the issue.

@quinedot currently I am testing the whole crate, and it looks like it's working fine (finally!).
I need to absorb the knowledge here.

Let me help with expanded macros:

// Recursive expansion of primitive_implementation! macro
// =======================================================

impl Archivedu32 {
        pub fn get(&self) -> u32 {

                self.0
        }
}

impl Serialize for u32 {
        type Input = Archivedu32;

        fn serialize_root(&self) -> Vec<u8> {

                alloc::vec::Vec::new()
        }

        fn serialize_value(&self, value_to_set: &mut Self::Input) {

                value_to_set.0 = *self;
        }
}

// Recursive expansion of archive_for_primitives! macro
// =====================================================

impl Archive for u32 {
        type Archived = Archivedu32;
}
// Recursive expansion of impl_tuplez! macro
// ==========================================

impl<T0, T1> Serialize for (T0, T1)
where
        T0: Serialize<Input = <T0 as Archive>::Archived> + Archive,
        T1: Serialize<Input = <T1 as Archive>::Archived> + Archive
{
        type Input = ArchivedTuple2<<T0 as Archive>::Archived, <T1 as Archive>::Archived>;

        fn serialize_root(&self) -> Vec<u8> {

                panic!("not yet implemented")
        }
       // modified for concision
        fn serialize_value(&self, value: &mut Self::Input) {
                
                self.0.serialize_value(&mut value.val.0);

                self.1.serialize_value(&mut value.val.1r);
        }
}

impl<T0, T1> Archive for (T0, T1)
where
        T0: Archive,
        T1: Archive
{
        type Archived = ArchivedTuple2<<T0 as Archive>::Archived, <T1 as Archive>::Archived>;
}

pub struct ArchivedTuple2<T0, T1> {
        val: (T0, T1)
}

I think it clicked: to use the Serialization trait, I also have to define the associated type because I use it in the serialize_value. Without the full definition, it could be anything.

So, from now on, whenever I use (an) associated type(s) in a trait, I should provide the full constraint for them all the time.

That way, the habit is taken and no more question about it. Even if not explicitly needed, or if the compiler doesn't complain.

That should take me a long way. Fingers crossed.

Thanks anyway, that journey has been great. And it's only the beginning :slight_smile:

Supply an associated type constraint if you need it, but don't if you don't, generally speaking.

Consider this method for example:

fn chain<U>(self, other: U) -> Chain<Self, <U as IntoIterator>::IntoIter>
where
    Self: Sized,
    U: IntoIterator<Item = Self::Item>,

It contrains IntoIterator::Item for equality with Self::Item, as it needs to return the same item type for Chain's entire iteration across both sub-iterators. But it doesn't constrain IntoIterator::IntoIter, since it can work with any iterator over Self::Item.

Ok, so I will let the compiler guide me instead.

Thanks!