Skip to content

Reconsider C-STRUCT-BOUNDS #217

@Kixunil

Description

@Kixunil

First let me say that C-STRUCT-BOUNDS talks about derived bounds mainly, which is completely fine, just a bit confusing (as the text down below speaks about bounds in general). I'd like to see a more visible distinction between the two, but that's not my main point here.

I suggest adding a new execption: "when a struct is supposed to wrap a type implementing a specific trait" - that means specialized wrappers for Iterator, Future, Stream, Read, Write etc. Reason: if the user attempts to instantiate the type without generic implementing the trait, he hits the error somewhere else, which is confusing and causes long error messages.

Requiring the trait will cause the error sooner, pointing at the correct location.

As an alternative, one might omit the bounds from struct itself and put them on every constructing function instead. This works until the crate author forgets about one case and exactly that case will be experienced by the user. (Murphy law: Everything that can break will break.)

#6 reasoned that it's annoying, but since generics are just type-level functions, it's equally annoying as having to write types in function signatures, which was very reasonably considered as a good thing.

Activity

Kixunil

Kixunil commented on Nov 2, 2020

@Kixunil
Author

Let me clarify this to make the rules clearer and maybe more obvious.

Terminology:

A type is considered Trait wrapper if:

  • it's generic
  • it implements Trait
  • nobody would write it if Trait didn't exist

A type is multi trait wrapper if

  • it's generic
  • implements TraitA, TraitB ...
  • nobody would write it if none of TraitA, TraitB ... existed

By "nobody would write it" I mean that there's no good reason to write it.
In other words, if S<T> is a generic type and rewriting it as type S<T> = T; would not remove any functionality besides the traits it wraps (and constructors), then nobody would write it without those traits existing.

I propose that:

  • All Trait wrappers have bounds on the type definition.
  • All multi trait wrappers have at least one constructor for each trait with that trait in the bound.

Examples:

#[Derive(Clone, Debug)]
struct IterFilter<I, F> where I: Iterator, F: FnMut(&I::Item) -> bool {
// ...
}

impl<T, F> Iterator for IterFilter<T, F> // ...

This is a combinator analogous to iterator.filter(). If the Iterator trait didn't exist, then this type would not provide any useful functionality. Deleting it and rewriting all occurrences to T would not change the behavior of the program. (As the only thing that it can do is clone and print itself. Debug impl would change but that's not really interesting/relevant.)

As we can see this type also impls Clone and Debug (via derive) but it'd still be useful without them existing.

Multi trait wrapper example:

// Can be used for Read, Write or both
struct Buffer<T> {
    io: T,
    read_buf: Vec<u8>,
    writ_buf: Vec<u8>,
}

impl<T: Read> Buffer<T> {
    fn new_reader(reader: T) -> Self {
        // ...
    }
}

impl<T: Write> Buffer<T> {
    fn new_writer(writer: T) -> Self {
        // ...
    }
}

impl<T> Read for Buffer<T> where T: Read // ...
impl<T> Write for Buffer<T> where T: Write // ...

This type provides useful functionality if Read exists but Write does not; if Write exists but Read does not; if both Read and Write exists.
If neither Read not Write exist, the type is completely useless.
Making sure that Buffer<T> can't be constructed if T doesn't impl Read nor Write makes sure user is informed about the fact at the point where it's constructed, not later where it can be confusing.

However, consider this alternative:

// Can be used for Read, Write or both
struct ReadBuffer<T> where T: Read {
    io: T,
    read_buf: Vec<u8>,
}

impl<T: Read> Buffer<T> {
    fn new(reader: T) -> Self {
        // ...
    }
}

impl<T> Read for Buffer<T> where T: Read // ...
impl<T> Write for Buffer<T> where T: Write // ...

This type implements buffering for readers only, not for writers. It additionally delegates writes if the inner type is a writer but does not buffer them.

Such type is still useless without Read existing because it can be replaced with T without loss of functionality.

I hope this explanation and the examples make the idea and reasons behind it more clear. I'll be happy to clarify if something is still not obvious.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    amendmentAmendments to existing guidelines

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @Kixunil@KodrAus

        Issue actions

          Reconsider C-STRUCT-BOUNDS · Issue #217 · rust-lang/api-guidelines