Idea: Trait+Impl Item for ergonomic extension traits

Occasionally one wants to create effectively inherent methods on foreign types for usage within their crate. When you do this, you have to define both a trait and an impl. All of the contents of the trait are just type signatures for the functions and then the impl contains the same type signatures but with an actual body. For example, say I want to add a method foo() to Option<MyType> to automatically create a new MyType if it's None and then call the method foo() on MyType. We have to write that out today as:

trait OptionMyTypeExt {
    fn foo(&mut self);
}

impl OptionMyTypeExt for Option<MyType> {
    fn foo(&mut self) {
        /* body of fn */
    }
}

Ideally I'd just like to write impl Option<MyType> but orphan rules get in the way, so we need the trait to have an item to have in scope for method resolution. But the trait being define separately doesn't actually help us. It duplicates the type signature so if we change it, we need to change it in two places. We never want to write a second implementation.

So, I propose a helper syntax where we just mash the trait and impl together:

trait OptionMyTypeExt impl Option<MyType> {
    fn foo(&mut self) {
        /* body of foo */
    }
}

This example creates the trait item and implementation item just like the original example. But, IMO, it has more benefits than just lowering the amount of duplication. By writing it like this, you give intent to the reader that this trait exists not to be implemented more than once, but just so it can be used. It also gives a single place for doc comments so you don't have to question whether to put them on the trait (where they're honestly more readable today in Rustdoc) away from the impl or next to the impl.

We could also choose to make the trait second class: We could just make it an item that when in scope, provides the methods for the method call operator. So you couldn't refer to it in a new impl or be generic over it. This would also disallow using generics on the trait itself, just the impl.

My only worry is that this would be yet another thing we'd have to teach about Rust and the gain in ergonomics would be marginal compared to that. But on the other hand, having an obvious almost-trivial way of creating inherent methods might open the doors to people using more of them. And if you just do the wrong thing and write the bare impl, the fix is just adding a keyword and a name. We could update the error index to show that and it'd be a pretty simple tool-assistable fix.

I'm pretty sure I could implement this in the compiler myself, and there are enough technical details that warrant a very detailed RFC, but before I think about that, I'd like to know the opinions of other Rustaceans if this is something I should even think about pursuing.

Before ending this, I'd like to just offer one more example. Itertool's defines a trait Itertool and has an implementation that is completely empty using default implementations in the trait. Their trait+impl could be combined using this proposal to be:

trait Itertool impl<T: Iterator> T { /*everything currently in the trait */ }
6 Likes

A somewhat related idea:

I might argue that for something as big as Itertools, the extra impl Itertools for impl Iterator {} line isn't a big deal. But for small, one-off things for local use, even the trait part is a pain, so I'd like to get right of that too.

Hmm, seems the procedural macro already exists, albeit with different syntax: https://docs.rs/crate/extension-trait/0.2.1

I'm not a fan of your desugaring because it doesn't let you group multiple associated items together or allow for things other than functions. It feels very specialized to the problem being solved.

For extremely local usage, we could make it so that you don't actually have to name the trait. Just leave it as an _: trait _ impl Vec<T> {}.

Edit: Oh, I see what you mean that the whole impl boilerplate is too much for local use. In that case, we could extend it to:

trait _ fn extension(ForeignType, /* other args */) {}

I don't know if eliminating that much boilerplate is worth it.

I would support a more ergonomic way of writing an extension method. So thanks for starting the conversation!

Using some sort of trait syntax shortcut seems natural as that's how we do it currently. But I had some other thoughts on this topic a little while ago, though I lacked the motivation to drive a conversation on it here. I may as well propose them now.

Example is_alex

First a simple motivating example. I can currently write a function in a module.

// foo.rs
pub(crate) fn is_alex(s: &str) -> bool {
    s.to_ascii_lowercase() == "alex"
}

This imports and is used quite naturally.

// main.rs
mod foo;
use foo::is_alex;

async fn is_alex_best_at_examples() -> Result<bool, reqwest::Error> {
    assert!(is_alex("Alex"));
    assert!(!is_alex("Barry"));

    let alex_is_best = is_alex(
        &reqwest::get("https://www.example.com/best-person")
            .await?
            .text()
            .await?,
    );

    Ok(alex_is_best)
}

But in this case I'd prefer to call this function in a fluent style foo.is_alex().

How to do this currently & why it isn't ideal for this use case

This can be done.

// foo.rs
pub trait IsAlex {
    fn is_alex(&self) -> bool;
}

impl<S> IsAlex for S where S: AsRef<str> {
    fn is_alex(&self) -> bool {
        self.as_ref().to_ascii_lowercase() == "alex"
    }
}

To import it though I need to use the trait name.

// main.rs
mod foo;
use foo::IsAlex;
...

This does already work which is a big plus. However I like less that:

  • I import using my trait name, when I only really care about the one function.
  • It's more syntax to define compared to plain function.

For me it's just about painful enough that I may prefer plain functions in many cases where the usage would be better as fluent style.

Idea: self implies extension method

How about my foo module looking like this?

// foo.rs
pub(crate) fn is_alex(self: &str) -> bool {
    self.to_ascii_lowercase() == "alex"
}

The self usage, which wouldn't compile currently, indicates this is an extension method called with .is_alex() but is otherwise identical to the normal function above.

The function imports in the same natural way.

// main.rs
mod foo;
use foo::is_alex;

async fn is_alex_best_at_examples() -> Result<bool, reqwest::Error> {
    assert!("Alex".is_alex());
    assert!(!"Barry".is_alex());

    let alex_is_best = reqwest::get("https://www.example.com/best-person")
        .await?
        .text()
        .await?
        .is_alex();

    Ok(alex_is_best)
}

Because we use very similar fn syntax we get:

  • Strong consistency with plain function, importing, generics usage etc.
  • self is fairly self-explanatory & self: Type usage already exists (self: Box<Self>).

So I think it has a low weirdness cost.

Issues

  • Trait / extension import collision. Should be an error?
  • Extra language complexity.

Related

2 Likes

Ouch. That breaks a fundamental current behavior, that argument names in free functions are part of the implementation and not the interface, and that changing argument names don't cause breakage. Doing this outside method context is very confusing.


Apart from that, I don't buy the inconvenience argument (as most of you might be used to it) – if the extension trait is really a small one-off trait, then typing up a function signature shouldn't be the reason for adding more syntax, because you can literally just copy and paste it.

As to indicating that a trait is an extension trait, naming convention (FooExt), or documentation, or both can be used.

1 Like

Given that self is not allowed in free functions at all currently, and is a keyword-like identifier that is known to change behaviour, and has exactly this current behaviour in all fn signatures that you can use it in, this doesn't seem like a big deal.

3 Likes

Maybe this is a slightly asinine response, but seeing as this can be accomplished with a simple macro (e.g. https://docs.rs/extend/0.1.2/extend/), I think this is the kind of thing best left to a library?

I'm also semi-not-a-fan of extension methods, unless we get a $expr.$path($exprs) production, so that folks can write

foo.OptionMyTypeExt::foo();

to disambiguate without having to deal with UFCS and how it totally breaks the niceness of chaining.

1 Like

That's something neat; I think the best first step is to already provide that functionality as a classic library crate through a procedural macro:

mod example {
    #[extension_method]
    pub(crate)
    fn is_alex<'__> (self: &'__ str) -> bool
    {
        self.eq_ignore_ascii_case("alex")
    }
}

fn main ()
{
    use core::ops::Not;
    use example::is_alex;
    assert!("Alex".is_alex());
    assert!("Lex Luthor".is_alex().not());
}
4 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.