Blog post: Tour of Rust's Standard Library Traits

I wrote Tour of Rust's Standard Library Traits to teach Rust beginners about all of the different traits in the standard library and how they are used!

Please let me know if you find anything confusing, unclear, or inaccurate! Your feedback is very important and helps me improve the article. Thanks!

Edit note: I don't actually go over every single trait in the standard library, just a curated list of traits which I think are the most commonly used and thus the must useful for beginners to learn about.

11 Likes

"Generic parameters" broadly refers to both generic type parameters and generic lifetime parameters.

Nowadays there’s also const generics. E.g.

trait Foo<const N: usize> {}

Regarding generics and traits, there might be value in trying to reproduce some of the gist of (my posts in) this thread – at least OP found it helpful there, this also plays into viewing traits with type arguments as a way to do multiple dispatch.

Depending on how long the “traits basics” is supposed to be you might want to mention orphan instances, too… oh wait

The rules Rust uses to enforce trait coherence, the implications of those rules, and workarounds for the implications are outside the scope of this article.

maybe you at least want to mention the term “orphan impl” or “orphan rules” or link to something like the reference and/or this post (which has more interesting links). Also, since you explain blanket impls, you might want to mention that adding blanket impls can result in semver-breaking changes while adding ordinary impls is usually not (considered) a breaking change.

Your object safety definition seems off, in particular

A trait method is object-safe if it meets these requirements:

  • method requires Self: Sized or
  • method does not use Self anywhere other than the first argument

a method without an actual self parameter/receiver isn’t object safe either, e.g.

trait Foo {
    fn foo(this: Self);
}

impl dyn Foo {}
2 | trait Foo {
  |       --- this trait cannot be made into an object...
3 |     fn foo(this: Self);
  |        ^^^ ...because associated function `foo` has no `self` parameter

you might want to copy/adapt the definition from the reference more closely.

I don’t really love the term “impl’d”, I think “implemented” might be nicer. It also corresponds to how rustc always speaks of “implement” or “implementation”.

Your prelude listing doesn’t contain IntoIterator (and maybe some more are missing). You should double-check the list / recreate it from the docs.

Almost all types are Send and Sync . The only notable Send exception is Rc and the only notable Sync exceptions are Rc , Cell , RefCell . If you need a Send version of Rc use Arc . If you need a Sync version of Cell or RefCell use Mutex or RwLock .

Maybe mention raw pointers not being send/sync and atomic types as an alternative for Cell (when applicable).


Edit: Another point

Functions

A trait function is any function whose first argument is not of type Self or some type which dereferences to Self , e.g. &Self , &mut Self , Box<Self> .

Methods

A trait method is any function whose first argument is of type Self or some type which dereferences to Self , e.g. &Self , &mut Self , Box<Self> .

Methods can be called using the dot operator on the implementing type:

That’s inaccurate. The relevant difference is whether there’s a (first) parameter called “self” (which is a keyword and can only be used for the first argument when the type is a valid receiver type). Also, valid receiver types (according to the reference) are only:

If the type of the self parameter is specified, it is limited to types resolving to one generated by the following grammar (where 'lt denotes some arbitrary lifetime):

P = &'lt S | &'lt mut S | Box<S> | Rc<S> | Arc<S> | Pin<P>
S = Self | P

The Self terminal in this grammar denotes a type resolving to the implementing type. This can also include the contextual type alias Self , other type aliases, or associated type projections resolving to the implementing type.

Also you define a “function”/“trait function” (which might IMO perhaps better be called an “associated function” (effectively) as a function that is not a method. Then you say

However methods are just functions with syntax sugar

It’s all a bit confusing; in my experience there is no real terminology for an associated funtion that is not a method. Also compare with the reference on these definitions/terminology.


Edit2:

However, the idiomatic refactor is actually:

fn example<I: Iterator<Item = i32>>(mut iter: I) {
   let first3: Vec<_> = (&mut iter).take(3).collect();
   for item in iter { // âś…
       // process remaining items
   }
}

I’d actually argue in favor of iter.by_ref().take(3).collect(). Also – even though it is useful to know that underscores can be used for this – I think that an explicit Vec<i32> annotation might be more clear in demonstrating that even though &mut or by_ref() is involved, the Item type stays the same.

Does that not include e.g. DoubleEndedIterator? It’s even in the prelude.

2 Likes

For Iterator, I would love a distinction between methods that one might want to overload and methods that one would always want to keep default implemented. I'm actually not really aware of any preexisting list of “potentially useful to overload” iterator methods anywhere online, and maybe there's some methods where it's not totally clear if you'd ever want to touch them, but I think something like this would be super useful. Useful to the point of: It should probably also be added to the documentation of Iterator itself. For some trivial examples: size_hint and nth would definitely be on the list and map and by_ref would not be on it.

Also maybe, to keep it short, in this tutorial you would only want to list a few most commonly overloaded methods of iterator including size_hint and maybe fold. And nth and last.

3 Likes

The blog post looks nice, good job! (haven't finished reading it).

One thing, though, before I forget: the prelude module used to be named v1, now it is called rust_20xx to match each edition, since Rust editions can now affect which prelude ends up used (for instance, Try{From,Into} will be in the 2021 edition) :slightly_smiling_face:


Second thing:

This could be an excellent occasion to mention (possibly through a footnote, to avoid info overload), that the derives in the standard library implement a trait:

  • if/when the type parameters (≠ types of the members) implement said trait,

  • and requires that the types of the members then implement said trait.

I'm saying this because it can be confusing (I personaly find it to be an inconsistency within the standard library derives' design) that:

#[derive(Clone, Copy)] // `Ptr_String : Copy`
struct Ptr_String(*const String);

// but:
#[derive(Clone, Copy)] // `Ptr<String>` is *not* `Copy`!
struct Ptr<S>(*const S);
3 Likes

It’s that kind of content that I like, I’ll follow the discussion here as well

What’s the motivation for overloading fold?

Feel free to steal anything from the following description of how Rust goes about iteration. It’s more describing the whole from the parts.

It increases performance for iterators where repeatedly calling next has overhead. The classical example is chain, where every single next call would have to execute a test of whether the iteration is currently still in the first iterator or still in the second. For deeply nested iterator chains this can even make a difference in terms of asymptotic execution time of an iteration.

Actually, that s just the benefit of overloading try_fold (once overloading try_fold even becomes possible). Overloading fold has the additional advantage that it becomes entirely unnecessary to track the progress of iteration inside of the iterator itself, since the iterator gets passed by-value and isn't needed anymore after the iteration which proceeds all the way until the end.

The choice of fold over e.g. for_each or others for overloading is also due to the fact that many other methods like sum or indeed for_each are defined using fold (in their default implementation).

Another example next to chain is x..=y inclusive ranges. Those use a flag internally to store if iteration has finished. The reason why something like this is necessary is that e. g. 0_u8..=255_u8 has a total of 257 iterator states, since it yields a total of 256 numbers followed by a None. Its next method needs to check this extra flag on every call, something like fold only needs to check it once in the beginning.

1 Like

... if overwritten or in what has already been implemented?

If overwritten. Well, for RangeInclusive the implementation is overwritten, so it's not much of an “if”, but if course I'm also implicitly talking about what reasons for doing so would be for custom iterators with similarities to RangeInclusive.

The default implementation of fold used to use try_fold in the past IIRC, but currently it's using repeated calls to next, so without any overloading of fold there's no advantage that fold can have over next.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.