Idiomatic use of Self

I am curious to learn how people tend to use (or not use) Self, and what are the arguments for using Self rather than the actual type.

I myself am pretty firmly in "no Self" camp. Partially this is just inertia on my part, but partially the desire to make the code more direct (the reader doesn't have to refer to impl header to understand what Self means).

However, most newer Rust code I see tends to use Self pretty liberally, not only for return types, but even when referring to enum variants and record literals. This gets a bit ridiculous, as I have to fight rust-analyzer's defaults here :smiley: Here's how ra now generates new and is functions:

pub struct S { x: u32, y: i128 }

impl S {
    pub fn new(x: u32, y: i128) -> Self {
        Self { x, y }
    }
}

pub enum Outcome { Ok, Err(Error) }

impl Outcome {
    pub fn is_ok(&self) -> bool {
        matches!(self, Self::Ok)
    }
}

So, what are your feelings on using Self wherever possible, as opposed to wherever necessary?

7 Likes

I feel the same way. It's mostly an aesthetic preference I think, but I do find myself taking the time to replace the generated Selfs with the named type.

2 Likes

For constructors/new Self seems correct.
Also for compare-like functions,as well as add/append functions.
This improves readabilty: is_equal(&self, other:&Self).

So, I tend to use Self whenever possible, mostly because it helps with refactoring ( which is not really a valid argument today because of RA).

In your is_ok example, I would also use Self.

7 Likes

I may allow myself to occasionaly be lazy and use Self in unexported signatures, but I never use it in exported signatures. IMO it harms the readability of docs too much.

But generally speaking, I almost never use Self. (I did go through a brief period of time where I did use it more, but I found it difficult to read that code when I came back to it.)

3 Likes

For constructors like new I really like -> Self, as it helps emphasize their constructor-ness.

That's especially true for things with generic arguments, since using Self avoids mistakes like using -> Vec<T> when you meant -> Vec<T, A>, or using -> Foo<'_> when it should really be -> Foo<'a>. As an example of the latter, using Self to avoid repeating the lifetime can help avoid needing to name lifetimes. And consistent Self usage really helps emphasize the times when you're using different generic params, and thus it's not Self.

When it's not used as a type I feel less strongly about it. Things like Self::TheVariant tends to more make me wish I could just write

impl Outcome {
    use Outcome::*;

    pub fn is_ok(&self) -> bool {
        matches!(self, Ok)
    }
}

since I'll probably be mentioning those variants a ton.

It's definitely convenient to not have to rename so many places if you change the name of a type, but that's also relatively rare and also not a problem for people using r-a anyway...

17 Likes

I tend to use Self when implementing traits that use it in their function signatures, simply to keep the symmetry. Like the -> Result<Self, Self::Error> in the GreaterThanZero example for TryFrom:

use std::convert::TryFrom;

struct GreaterThanZero(i32);

impl TryFrom<i32> for GreaterThanZero {
    type Error = &'static str;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value <= 0 {
            Err("GreaterThanZero only accepts value superior than zero!")
        } else {
            Ok(GreaterThanZero(value))
        }
    }
}

I would find it weird to write it as -> Result<GreaterThanZero, &'static str>.

4 Likes

I tend to use it a lot, including for associated types like with

fn next(&mut self) -> Option<Self::Item> { /* ... */ }

as

  • it matches the trait
  • it is in line with the abstraction
  • it is the same for every implementation

(which are all interrelated points).

I don't really use Self for enum variants though.

8 Likes

I'm mostly in the same camp as @quinedot. I like -> Self in constructors, and I like to exactly match the type signature in traits when dealing with associated types, so instead of Result<Foo, MyError>, Result<Self, Self::Error> it is. I also like to use Self when specifying an associated function in a method, as in self.iter.map(Self::frobnicate).

No Selves in enum variants and struct literals, though (so I write ExplicitName { fields } instead of Self { fields }).

6 Likes

The only times I will use Self are,

  1. When defining a constructor (e.g. fn new(...) -> Self or fn load(...) -> Result<Self, Error>) because the Self return is more easily recognizable as a constructor when looking through docs
  2. When it is obvious that an argument should be the same type (e.g. imagine a fn dot(self, other: Self) -> f32 method for doing the dot product on a Point type)
  3. Traits and generics (e.g. where Self: ...)

I don't really see the point of using Self in a function body or outside a signature. 90% rust-analyzer will autocomplete the name or match statement for you, so it's not like you've reduced the amount of typing you need to do.

2 Likes

I'm "Self all the time everywhere" camp.

Not just because it's easier to type and change, but also for readability. When I see Self I know it's the type of the impl being used, and not some 3rd type referred to.

Self reference also includes exact lifetimes of the impl. When referring to the type by its name, you may get different lifetimes inferred.

40 Likes

I use Self only when it's not easy to type, so something like new will probably never be Self since it harms both the docs and code readibility.

Docs as in it broke searchability, when user search it by return types, Self cannot be searched (like if you search for -> Type), also readability when the docs gets very long the Self you have to scroll pages and pages to figure out what that Self means, and even worse when there are like ~5 impl of the same type. See Vec in std::vec - Rust for an example of long docs with lots of different impls but luckily that doesn't use Self.

Code readability as in reading from the source code, especially true if you have many constructors with one type having 20-30 functions and have like more than 5 impl in the same file, when you look at a function, you have no idea what Self means, I have to scroll backward page by page (worse ones like 10 page backwards) to find what that Self means.

Due to these reasons I tend to avoid Self when possible, unless like the type is really hard to type like with some lifetimes or it's associated type then Self::Error those should be fine I guess. Being slightly explicit here when writing return types save my brainpower to remember the context of what it is implemented for.

Give the amount of :heart: kornel's answer received, I guess rust-analyzer is using the right defaults, though I probably should add an option for the likes of BurntSushi and myself to not use Self :rofl:

2 Likes

Same. Though there are some inconsistencies (from a human point of view) that force me not to use it in some situations where I rather would. For example use Self::* in fn fmt() and fn source() of an error enumeration to make matching less verbose doesn’t work as I’d like.

I honestly don’t get the argument that Self makes things less readable. In general I find the opposite, especially coming from C++ where there isn’t a simple alternative to repeating the type name everywhere.

I'm of that camp as well; looking at a PR's diff or some grep result to see Self in signatures is just so annoying.

  • But I do reckon that Self as an actual alias, such as in constructors, is useful, and may actually avoid type inference errors should the author of the function make the mistake to leave some generic non-lifetime parameters unconstrained.

    /// Doing `MyHashSet::with_default_hasher()` will lead to inference errors with `S`.
    impl<K, S> MyHashSet<K, S> {
        fn with_default_hasher() -> MyHashSet<K, RandomState>
    

But the moment I'm reading code, or the documentation of a crate, and the signature becomes a bit more complex (e.g., scoped threads API), I waste so much time performing that Self alias substitution in my head just to be able to fully unveil a function signature. It's so silly that the current situation forces the reader to perform that mechanical operation just to keep some aliases around.

  • Incidentally, it's a similar issue with type aliases: sometimes they improve the readability by conveying some extra information, and in other cases, they just make the signature less self-contained and require that the reader click on a bunch of aliases and create a mental map of the substitutions just to read the signature.

That's why, one thing I'd love, independently of the "written code style" w.r.t. Self discussed herein, is for there to be to a togglable option, in rustdoc, to replace Self with what it refers to.


One reason I dislike Self, for instance, is the common beginner antipattern of:

impl MyThing<'a> {
    fn get(&'a self) -> &'a Field

which, when get is far from the impl, is easy/ier to miss.


Regarding function bodies, the Self-as-a-shortcut can also lead to strange error messages.

Take the following snippet, for instance:

enum MyLengthyCowName<'str> {
    Owned(String),
    Ref(&'str str),
}

impl MyLengthyCowName<'_> {
    fn reborrow<'r>(&'r self) -> MyLengthyCowName<'r> {
        match *self {
            | Self::Ref(it) => Self::Ref(it),
            | Self::Owned(ref it) => Self::Ref(&**it),
        }
    }
}

yields:

error[E0495]: cannot infer an appropriate lifetime for pattern due to conflicting requirements
  --> src/lib.rs:10:27
   |
10 |             | Self::Owned(ref it) => Self::Ref(&**it),
   |                           ^^^^^^
   |
note: first, the lifetime cannot outlive the lifetime `'r` as defined here...
  --> src/lib.rs:7:17
   |
7  |     fn reborrow<'r>(&'r self) -> MyLengthyCowName<'r> {
   |                 ^^
note: ...so that reference does not outlive borrowed content
  --> src/lib.rs:10:27
   |
10 |             | Self::Owned(ref it) => Self::Ref(&**it),
   |                           ^^^^^^
note: but, the lifetime must be valid for the lifetime `'_` as defined here...
  --> src/lib.rs:6:23
   |
6  | impl MyLengthyCowName<'_> {
   |                       ^^
note: ...so that the types are compatible
  --> src/lib.rs:10:38
   |
10 |             | Self::Owned(ref it) => Self::Ref(&**it),
   |                                      ^^^^^^^^^
   = note: expected `MyLengthyCowName<'_>`
              found `MyLengthyCowName<'_>`

For more information about this error, try `rustc --explain E0495`.

where doing:

-           | Self::Owned(ref it) => Self::Ref(&**it),
+           | Self::Owned(ref it) => MyLengthyCowName::Ref(&**it),

fixes the issue.

And this kind of illustrates the issue I have with Self aliases: in the mind of some people, or even anyone when a bit unattentive, it's easy to picture that Self stands for MyLengthyCowName, for instance, or, in general, for the path of the type / for the type constructor. That is, Self in a impl<T> Vec<T> may be seen, loosely, as representing Vec, when it actually represents Vec<T, GlobalAlloc>, and similarly, Self was standing for MyLengthyCowName<'lt>, with 'lt being the '_-anonymous lifetime introduced at the impl level.

In these situations, Self can end up being an overly terse way to hide / forget of generic parameters, and even more so as the impl line that introduced its ever changing definition is far.

And while for type parameters this doesn't happen that often, I think it's a pretty common mistake the moment lifetime parameters are involved.

So I guess my personal heuristic is, besides the very specific new() -> Self case, not to use Self the moment it's hiding a lifetime parameter :thinking:

5 Likes

:100:

It would be wonderful to be able to see both the "conceptual model" as well as the "actual details" for various signatures.

One that jumps to mind is Iterator::try_find, where in code the return type is written

ChangeOutputType<R, Option<Self::Item>>

but which RustDoc shows expanded as

<<R as Try>::Residual as Residual<Option<Self::Item>>>::TryType

And both of those are useful, with the former being clearer about what it's trying to do and the latter being clearer about how it's doing it.

I wonder if this hypothetical feature expanding out lifetime elision would make sense too (like adding explicit names to anything tied together, and using '_ only for single-use free lifetimes).

7 Likes

To elaborate a bit on @kornel's take:

I've started enabling clippy's use_self lint to enforce the use of Self in some of the code bases I work on. I like that it's almost always more concise than the actual type name, and that it doesn't result in changes if I want to rename a type.

1 Like

I also find myself in the Self all the time everywhere camp.

Mostly because the code doesn't need to change when you rename the type you're implementing on. For quick iterations on code that changes a lot, I find it more efficient to think about the code when seeing something familiar in the impl block, rather than the result of a refactor that looks unfamiliar.

1 Like

I happen to be in the "use Self everywhere all the time" camp as well.
It's quite useful when refactoring, which I tend to do often.

I deal with this by mainly reading code in my editor. In any decent editor, when it is unclear you can just jump to definition to see what Self actually is.

As for docs, I've never really had a problem with Self there because the only places it can occur are the pages of types and traits, in which case all I need to do to know what Self is is either remember what item I navigated to, or just look at the URL bar of my browser.

That said, for people who have trouble with it when reading docs, perhaps an option could be added to mechanically transform the occurrences of Self to the actual type. So the sources would say Self, but the docs would name the actual type complete with any and all generic parameters.

I don't really want my docs (or formater) to normalize away from what I coded. But some sort of annotation would be useful ala "notable traits" (in either direction: "concrete type of Self / Self::Associated" and "this concrete type happens to be Self / <Self as ThisTrait>::Associated").

1 Like

The idea would be that it would be opt-in (eg a switch to cargo doc), so the choice would always be in the hands of the user.