I’m posting this to figure out of the idea has been proposed before, and if it’s worth expanding to a full RFC. I didn’t find anything similar on this forum or in the RFCs repo, but I could have missed something. It’s a bit rough with only headings for some Alternatives and Drawbacks, mostly to save on effort at this point.
- Feature Name: type_parameter_elision
- Start Date: (fill me in with today’s date, YYYY-MM-DD)
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
Summary
Allow eliding type parameters with trait bounds in function signatures where there are no relationships between the parameters.
Motivation
Generic functions pay a big syntactic noise penalty compared to
non-generic functions. Consider a function taking &str
fn foo(s: Vec<u8>)
We could make it more generally applicable by having it accept an
Into<Vec<u8>>
type, but that requires writing either
fn foo<S: Into<Vec<u8>>>(s: S)
or
fn foo<S>(s: S) where S: Into<Vec<u8>>
This RFC proposes allowing
fn foo(s: Into<Vec<u8>>)
This mirrors lifetime elision introduced in RFC 141.
In particular, this would make signatures for many functions accepting
closures quite a bit nicer to read and write. For example,
slice::binary_search_by
could change from
binary_search_by<F>(&self, f: F) -> Result<usize, usize> where F: FnMut(&T) -> Ordering
to
binary_search_by(&self, f: FnMut(&T) -> Ordering) -> Result<usize, usize>
Detailed design
Bare trait bounds would be accepted in function signatures anywhere a type parameter could appear.
-
each such appearance introduces a type parameter bounded by the trait bound
-
introduced type parameters appear after all explicitly provided type parameters
-
bare trait bounds may only reference explicitly provided type parameters, or to type parameters in scope from outer
impl
s,trait
s, orfn
s.
Examples
Here are a few examples from the standard library.
impl<T> Option<T> {
pub fn unwrap_or_else(self, f: F: FnOnce() -> T) -> T { ... } // elided
pub fn unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T { ... } // expanded
pub fn map<U>(self, f: FnOnce(T) -> U) -> Option<U> { ... } // elided
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> { ... } // expanded
}
impl CString {
pub fn new(t: T: Into<Vec<u8>>) -> Result<CString, NulError> { ... } // elided
pub fn new<T: Into<Vec<u8>>>(t: T) -> Result<CString, NulError> { ... } // expanded
}
pub trait FromIterator<A> {
fn from_iter(iterator: IntoIterator<Item=A>) -> Self; // elided
fn from_iter<T>(iterator: T) -> Self where T: IntoIterator<Item=A>; // expanded
}
impl <T> [T] {
pub fn binary_search_by<T>(s: &[T], f: FnMut(&T) -> Ordering) -> Result<usize, usize> { ... } // elided
pub fn binary_search_by<T, F>(s: &[T], f: F) -> Result<usize, usize> // expanded
where F: FnMut(&T) -> Ordering { ... }
}
impl str {
pub fn trim_left_matches<'a>(&'a self, pat: Pattern<'a>) -> &'a str { ... } // elided
pub fn trim_left_matches<'a, P>(&'a self, pat: P) -> &'a str where P: Pattern<'a> { ... } // expanded
}
Drawbacks
Provides more than one way to write the same thing
Introduces additional subtleties
Alternatives
Status quo
Unresolved questions
Precision in rules for elision and expansion
Ability to elide type parameters for reference parameters
AsRef
is a really useful place for this kind of elision, but it is used
on references. As an example:
impl Path {
pub fn new<S: AsRef<OsStr> + ?Sized>(s: &S) -> &Path { ... }
}
To elide the S
parameter with the scheme in this RFC, we’d need to write
pub fn new<S: AsRef<OsStr> + ?Sized>(s: &AsRef<OsStr> + ?Sized) -> &Path { ... }
which is a trait object type.
Grammar and parser support
I don’t know if it’s possible or easy to disabiguate
fn foo(s: SomeStruct<String>)
from
fn foo(s: SomeTrait<String>)
Accepting traits in other positions
We could imagine allowing a trait bound to appear anywhere a type parameter can appear in signatures.