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.......
-
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 (
:
T: Add<U> + Mul<T::Output>
,:
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.
-
-
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 likeFromIterator
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 asi32
. -
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 toNeg<Output = i32>
.Add(i32) -> i32
would be equivalent toAdd<i32, Output=i32>
. Bound type isSelf
.Fn(A, B) -> C
would be equivalent toFn<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 likelyfn
, 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 withimpl Trait
, or possibly even the featurereturn_type_notation
.For example, if the
Iterator
trait had the defaulttype 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> { // .. } }