[Idea] Function trait impls

This is just a concept that I wanted to get out of my head. I don't plan on writing any kind of RFC, but if you like any ideas here feel free to use them or give your own. The downsides are probably too particular for this to work in Rust, but I think it's neat.

The Pattern

Many (most?) traits seem to follow this pattern: a single required function/method, often defined alongside one or more associated types to be used in its signature. If the trait has generic parameters, those are often also used in the function signature or associated types.

trait Foo<A, B, ...> {
	type Output;
	fn foo(self, a: A, b: B, ...) -> Self::Output;
}

Most notably, in the standard library we have:

  • ops::{Add, Sub, Mul, Div, Neg, Deref, Index, Drop} and many more.
  • iter::{Iterator, IntoIterator} and a few more.
  • convert::{AsMut, AsRef, From, Into, TryFrom, TryInto}.
  • cmp::{Ord, PartialEq, PartialOrd}.
  • clone::Clone.
  • default::Default.

I notice this pattern in plenty of user-defined crates as well, including my own projects. Generally anytime you want a trait that represents a simple function, it appears.

The Motivation

I argue that because these traits are incredibly common, it's also incredibly common for users to implement them and use them as bounds. Therefore, these traits compel such a user to either:

  • A: Repeat a large amount of boilerplate code.
  • Or B: Use macros that generate the impls with automated boilerplate.

Option B is acceptable and I use it often - however: I find this pattern to be so common that I believe it deserves an official syntax. My reasoning is as follows.......

  1. As an interface:

    • The average user has little reason to care about the names of items in a 1-function trait. Rather, they care about the overall functionality that it provides through existing systems (operators, for loops, iterator methods, conversion, comparison, etc.).

      Unfortunately, in the general case, there's no way to elide these item names even with macros. At best a macro can handle this by convention, and at worst case-by-case.

    • Associated types are often named generically because of the desire for codegen (I assume). Unfortunately this also makes them more ambiguous for users and compilers (:cross_mark:: T: Add<U> + Mul<T::Output>, :hamburger:: T: Add<U> + Mul<T::Sum>).

      And while existing associated types can't really be renamed, I would prefer to give my own associated types meaningful names without worrying about codegen.

  2. As source code:

    • Macro-generated impls are inherently more difficult to comprehend because you first have to expand the macro into a normal impl, or at least learn its syntax. Not the worst, not the best, but it would nice to have a standard syntax with more consistency across the board.

    • Aligning 1-function impls across multiple lines is fairly impractical. I find this to be a major annoyance; consider:

      impl<A: Add<B>, B> MyAdd<B> for A { type Output = A::Output; fn my_add(self, rhs: B) -> Self::Output { self + rhs } }
      impl<A: Sub<B>, B> MySub<B> for A { type Output = A::Output; fn my_sub(self, rhs: B) -> Self::Output { self - rhs } }
      impl<A: Mul<B>, B> MyMul<B> for A { type Output = A::Output; fn my_mul(self, rhs: B) -> Self::Output { self * rhs } }
      impl<A: Div<B>, B> MyDiv<B> for A { type Output = A::Output; fn my_div(self, rhs: B) -> Self::Output { self / rhs } }
      

      This is a lot of syntax, and understanding it - let alone changing it - requires a lot of effort (particularly when it isn't single-lined to make repetition obvious). As compared to some pseudo-code you might write:

      MyAdd!(A, B) -> Add!(A, B)
      MySub!(A, B) -> Sub!(A, B)
      MyMul!(A, B) -> Mul!(A, B)
      MyDiv!(A, B) -> Div!(A, B)
      

      It may be more ambiguous to a compiler, but it's much clearer to any human.

    • For the purposes of prototyping, the verbosity in a simple impl is a relatively large amount of friction. I feel you should be able to quickly test & modify ideas that you aren't committed to, and easily intuit those ideas back from the code; simple ideas deserve simple syntax.

The Idea

Traits that require a singular [non-generic] function, and where any & all of the associated types are constrained by that function's signature, are considered "function traits".

Function traits can be implemented for types using a specialization of impl syntax. When an impl trait is followed by a function parameter list and optional return type, the impl block becomes the function's body, and all of its associated types are inferred from the parameters & return type:

impl GenericParams? TypePath(FunctionParameters?) FunctionReturnType? for Type
WhereClause
BlockExpression

As a convenience, any generic parameters of the trait can be omitted to be inferred from the function parameters & return type as well (if unambiguous).

Examples:

  • std::ops::Add<Rhs>:

    impl Add<i32>(self, rhs: i32) -> i32 for i32 {
        self + rhs
    }
    
    // Equivalent to:
    impl Add<i32> for i32 {
        type Output = i32;
        fn add(self, rhs: i32) -> Self::Output {
            self + rhs
        }
    }
    

    As seen here, the list of generic parameters (<i32>) is applied to the trait; not the function. Traits with a single generic function like FromIterator would be excluded for this reason.

  • iter::Iterator:

    impl Iterator(&mut self) -> Option<i32> for I32Iter {
        self.index += 1;
        self.nums.get(self.index)
    }
    
    // Equivalent to:
    impl Iterator for I32Iter {
        type Item = i32;
        fn next(&mut self) -> Option<Self::Item> {
            self.index += 1;
            self.nums.get(self.index)
        }
    }
    

    As seen here, the output type is constrained by the parameter of Option<T> and can be unambiguously determined as i32.

  • convert::From:

    impl From(value: i32) -> Self for Option<i32> {
        Some(value)
    }
    
    // Equivalent to:
    impl From<i32> for Option<i32> {
        fn from(value: i32) -> Self {
            Some(value)
        }
    }
    

    As seen here, the parameter of From<T> is omitted and inferred from the function parameter.

Aside: I'm not sure what you would do with any "effects" on the function (const, async, etc.). They could be implicit, but that might be a downside in terms of source code readability/confusion. Perhaps you would disallow them altogether? I can't imagine there would be that many cases where it would come up, and most function traits would probably end up using the const Trait feature (and others) when it eventually comes around. At worst you can just use a normal impl.

The Peculiar Possibilities

  • Traits with a single [generic] function could be supported with a for<_>-style syntax, as in:

    impl<A> for<T: IntoIterator<Item=A>> FromIterator<A>(iter: T) -> Self for Vec<A> {
        // ..
    }
    
    // Equivalent to:
    impl<A> FromIterator<A> for Vec<A> {
        fn from_iter<T: IntoIterator<Item=A>>(iter: T) -> Self {
            // ..
        }
    }
    
  • The special syntax of the Fn* traits could be generalized to all function traits, as defined.

    • Neg() -> i32 would be equivalent to Neg<Output = i32>.
    • Add(i32) -> i32 would be equivalent to Add<i32, Output=i32>. Bound type is Self.
    • Fn(A, B) -> C would be equivalent to Fn<A, B, Output=C>, implying variadic generics?

    At the same time, the "impl function trait" syntax could potentially provide a stable avenue for implementing the Fn* traits for normal types without needing variadics. I have no idea! (impl Fn(a: i32, b: i32) -> i32 for I32Adder { a + b })

The Upsides

  • This would reduce friction in early development, help communicate big picture ideas clearly, and broadly make coding easier & more enjoyable as a result - so I claim.

    Refactoring the earlier wall-of-impls example, I'd argue it moves closer to the pseudo-code in terms of clarity while still being unambiguous:

    impl<A: Add<B>, B> MyAdd(self, rhs: B) -> A::Output for A { self + rhs }
    impl<A: Sub<B>, B> MySub(self, rhs: B) -> A::Output for A { self - rhs }
    impl<A: Mul<B>, B> MyMul(self, rhs: B) -> A::Output for A { self * rhs }
    impl<A: Div<B>, B> MyDiv(self, rhs: B) -> A::Output for A { self / rhs }
    
  • Macro-generated impls would be easier to read and write for function traits, not needing to name their inner items whatsoever. They can produce this syntax; the compiler handles it.

  • Fairly painless to convert between function trait impls and normal trait impls. You can reuse the parameter list, return type, and body for the function, and then specify any associated types & generic parameters as needed.

The Downsides

  • Users would have to learn a new syntax for impls, which increases language complexity. Most notably, the function trait impl using a block expression, rather than a block of items, could very well cause confusion for anyone unfamiliar.

  • This effectively adds a new common syntax for declaring functions in general, which presently is really only done using fn item syntax. This could be undesirable for various reasons.

  • Adds a choice for how to define an impl, which might not be obvious for every user in every case. I'd argue you should always go with the more abstract one unless you need more control or explicitness, like most syntax sugar, but it's still a choice you have to think about.

The Alternatives

  • The syntax could be adjusted to require a keyword before (or after) impl, most likely fn, to clarify that this is a more specific kind of impl related to functions.

    impl fn Add(self, rhs: i32) -> i32 for i32 {
        self + rhs
    }
    
  • The syntax could be adjusted to include the name of the function being implemented.

    This would make it straightforward to add support for traits with generic functions, like FromIterator::from_iter<T>. It may also be desirable for traits that don't share a name with their function, particularly when it's commonly referenced by users directly, e.g.

    impl Iterator::next(&mut self) -> Option<i32> for I32Iter {
        // ..
    }
    

    However, for the most part I believe these cases to be fairly niche and that this syntax should primarily remain a convenient entry point; a simple brush for simple things.

  • You could move the function body between the return type and the for keyword, and allow the impl block to behave as normal - for overriding provided methods and whatnot. However, this could look confusing and needlessly stretches the purpose of the syntax.

  • The motivation could be partly solved by the feature associated_type_defaults if it could be combined with impl Trait, or possibly even the feature return_type_notation.

    For example, if the Iterator trait had the default type Item = impl Any;, this could potentially allow you to specify the omitted associated type within the function's signature.

    impl Iterator for I32Iter {
        fn next(&mut self) -> Option<i32> {
            // ..
        }
    }
    
2 Likes

That very last bullet point would make this mostly superfluous for me: if Rust would infer an associated type from a function requirement, then the short form is only saving the name of the function, a pair of braces, and some indentation over the long form, at the cost of not being able to handle generics or modifiers very well. But I don’t know how far we are from that being a reality. (It is a thing in Swift, but it was given too much power there and can lead to weird results; Rust shouldn’t have those problems as long as the type has to exactly match a piece of another declaration in the impl…for block.)

It would be clearer to me if it included both the fn keyword and the method name, like this

impl fn Add::add(self, rhs: Self) -> Self for i32 {
    self + rhs
}

The more I look at this, the more I like it.

Also, the syntax suggests we could separate the same impl in multiple items, perhaps in different modules (but in the same crate), by using this feature even if the trait had many methods.

6 Likes

I think I'd come at this problem from a different direction. I don't think it needs different syntax.

Rather, the annoyance for me in most of these is how often you have to repeat stuff, but I think we could address that in other ways.

Part 1

For example, repeating the type in

impl From<Some<Really, Long<Thing>>> for Other<Big, Thing> {
    fn from(other: Some<Really, Long<Thing>>) -> Self { ... }
}

is obnoxious and unnecessary. At least for the return type there's Self, but there's nothing for other types.

But that type is fixed by the trait, so what if we let you just leave it off?

impl From<Some<Really, Long<Thing>>> for Other<Big, Thing> {
    fn from(other) -> Self { ... }
}

(Or other: _ if you prefer)

Part 2

Similarly, the type Output = …; is set by the function, so we could allow just

impl Add for Foo {
    fn add(self, other) -> Self { … }
}

where the Output gets set to Self because of course it does.

It's not even really a new mechanism to do that, because it's essentially a more restricted form of inference than TAITs.

Part 3

Then the other part comes when you have a whole bunch of these to do. Notably, though, the "look like a function" syntax doesn't really help simplify the multiple, just makes it a big shorter per.

Imagine if you could implement multiple traits at once, say something like this:

impl Add + Sub + Mul for Foo {
    fn add(self, other) -> Self { … }
    fn sub(self, other) -> Self { … }
    fn mul(self, other) -> Self { … }
}

Or though a trait alias.

Lang has recently been discussing wanting implementing supertrait methods through an impl block for the subtrait to work, and this feels similar -- for example you could just impl num_traits::NumOps for Foo { ... } and put all the necessary functions in there.

17 Likes

Honestly these ideas together seem like they'd fit the language much more smoothly, I love them.

They also solve another minor problem I encounter: recently I've started organizing impls into modules per type or trait (mod _impl_trait, mod _foo_impls), just because they clog up the file structure. One big multi-trait impl per type would essentially organize them automatically.

Edit: There is one problem with the third idea though; how would you have separate generic paramters and bounds per trait impl? I suppose you could put a where clause right after each trait. I'm not sure, but I think this would matter for the vast majority of my use cases.

I like all your concrete suggestions. However they do not address the thing I find most frustrating about writing one-function trait impls, which is that I always, always have to go back to the documentation to remind myself of the name of the fn inside the trait. I know I need to impl From, say, but so far the fact that it's fn from and not something else has not managed to stick, and it feels like it is something I shouldn't have to remember.

Having to repeat the types a bunch is minor by comparison because I don't have to go back to the docs for them, normally. (Something like TryFrom I will probably have to remind myself if it returns Result or Option or what, but that's not as annoying because I probably want to refresh myself on the expected behavior on failure anyway.)

Honestly, I'd go crazy writing trait impls if the IDE wasn't there to auto-complete the trait interface for me. The computer already knows what should be there so I can focus on important things rather than remembering minutiae.

1 Like

How about fn +(a:A, b:B) -> C { ... }?

Something like this would be possible with macros today but it would have to know all the traits you want to implement

struct Foo {
    a: i32,
    b: i32,
}

#[quick_impl]
impl Foo {
    // automatically infer trait to be `Add<Self, Output = Self>`
    #[impl(Add)]
    fn add(mut self, rhs: Self) -> Self {
        self.a += rhs.a;
        self.b += rhs.b;

        self
    }
    #[impl(From)]
    fn from((a, b): (i32, i32)) -> Self {
        Self { a, b }
    }
    #[impl(Into)]
    fn into(self) -> (i32, i32) {
         (self.a, self.b)
    }
 }
4 Likes

TBH I'd just say you don't. You'd write

impl<T: NumOps> NumOps for Foo<T> {
    fn add …;
    fn sub …;
    fn mul …;
    fn div …;
}

and just say "meh, don't care" to types that are Sub and Mul but not Add.

If everything needs different bounds, then I don't mind saying you have to write them all out separately, since if they're different you'd always have to say which bounds go with which traits, and that'll always be somewhat verbose.

I don't know if it works grammatically, but I've imagined a

impl Add<Rhs @ Some<Really, Long<Thing>>> for Foo {
    fn add(rhs: Rhs) { ...}
}

syntax. Then you also have a nice way to name the type elsewhere, like associated types of in the function body.

4 Likes

I'd love that. Some syntax for "hey, it's helpful to define this type variable, but it's private to the implementation and not turbo-fishable" would be wonderful.

Makes me think of Inference worse in method signature using associated type than with new generic type parameter eq-constrained to the associated type · Issue #45462 · rust-lang/rust · GitHub where more generic types, even if they shouldn't be necessary, can actually make type inference work better.

Musing on syntax: I don't know if we want to allow them "in-band" that way. Might want to keep the declaration-and-use separation with something more like

impl<priv Rhs = Some<Really, Long<Thing>>> Add<Rhs> for Foo {
    type Output = Rhs;
    fn add(rhs: Rhs) { ...}
}

(Oh, cool, the highlighter even knows that priv is still a reserved keyword.)