- Start Date: 2015-01-22
- RFC PR: rust-lang/rfcs#587
- Rust Issue: rust-lang/rust#21527
Summary
The Fn
traits should be modified to make the return type an associated type.
Motivation
The strongest reason is because it would permit impls like the following (example from @alexcrichton):
impl<R,F> Foo for F : FnMut() -> R { ... }
This impl is currently illegal because the parameter R
is not
constrained. (This also has an impact on my attempts to add variance,
which would require a “phantom data” annotation for R
for the same
reason; but that RFC is not quite ready yet.)
Another related reason is that it often permits fewer type parameters.
Rather than having a distinct type parameter for the return type, the
associated type projection F::Output
can be used. Consider the standard
library Map
type:
struct Map<A,B,I,F>
where I : Iterator<Item=A>,
F : FnMut(A) -> B,
{
...
}
impl<A,B,I,F> Iterator for Map<A,B,I,F>
where I : Iterator<Item=A>,
F : FnMut(A) -> B,
{
type Item = B;
...
}
This type could be equivalently written:
struct Map<I,F>
where I : Iterator, F : FnMut<(I::Item,)>
{
...
}
impl<I,F> Iterator for Map<I,F>,
where I : Iterator,
F : FnMut<(I::Item,)>,
{
type Item = F::Output;
...
}
This example highlights one subtle point about the ()
notation,
which is covered below.
Detailed design
The design has been implemented. You can see it in this pull
request. The Fn
trait is modified to read as follows:
trait Fn<A> {
type Output;
fn call(&self, args: A) -> Self::Output;
}
The other traits are modified in an analogous fashion.
Parentheses notation
The shorthand Foo(...)
expands to Foo<(...), Output=()>
. The
shorthand Foo(..) -> B
expands to Foo<(...), Output=B>
. This
implies that if you use the parenthetical notation, you must supply a
return type (which could be a new type parameter). If you would prefer
to leave the return type unspecified, you must use angle-bracket
notation. (Note that using angle-bracket notation with the Fn
traits
is currently feature-gated, as described here.)
This can be seen in the In the Map
example from the
introduction. There the <>
notation was used so that F::Output
is
left unbound:
struct Map<I,F>
where I : Iterator, F : FnMut<(I::Item,)>
An alternative would be to retain the type parameter B
:
struct Map<B,I,F>
where I : Iterator, F : FnMut(I::Item) -> B
Or to remove the bound on F
from the type definition and use it only in the impl:
struct Map<I,F>
where I : Iterator
{
...
}
impl<B,I,F> Iterator for Map<I,F>,
where I : Iterator,
F : FnMut(I::Item) -> B
{
type Item = F::Output;
...
}
Note that this final option is not legal without this change, because
the type parameter B
on the impl would be unconstrained.
Drawbacks
Cannot overload based on return type alone
This change means that you cannot overload indexing to “model” a trait
like Default
:
trait Default {
fn default() -> Self;
}
That is, I can’t do something like the following:
struct Defaulty;
impl<T:Default> Fn<()> for Defaulty {
type Output = T;
fn call(&self) -> T {
Default::default()
}
}
This is not possible because the impl type parameter T
is not constrained.
This does not seem like a particularly strong limitation. Overloaded
call notation is already less general than full traits in various ways
(for example, it lacks the ability to define a closure that always
panics; that is, the !
notation is not a type and hence something
like FnMut() -> !
is not legal). The ability to overload based on return type
is not removed, it is simply not something you can model using overloaded operators.
Alternatives
Special syntax to represent the lack of an Output
binding
Rather than having people use angle-brackets to omit the Output
binding, we could introduce some special syntax for this purpose. For
example, FnMut() -> ?
could desugar to FnMut<()>
(whereas
FnMut()
alone desugars to FnMut<(), Output=()>
). The first
suggestion that is commonly made is FnMut() -> _
, but that has an
existing meaning in a function context (where _
represents a fresh
type variable).
Change meaning of FnMut()
to not bind the output
We could make FnMut()
desugar to FnMut<()>
, and hence require an
explicit FnMut() -> ()
to bind the return type to unit. This feels
surprising and inconsistent.