Idea: Trait+Impl Item for ergonomic extension traits

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