Closed aturon closed 5 years ago
In fn func(t: T) -> V
, no need to distinguish any t or some v, so as trait.
fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
move |x| g(f(x))
}
still works.
@J-F-Liu I am personally opposed to having any
and some
conflated into one keyword/sigil but you are technically correct that we could have a single sigil and use it like the original impl Trait
RFC.
@J-F-Liu @eddyb There was a reason sigils were removed from language. Why that reason wouldn't apply to this case?
@
is also use in pattern matching, not removed from the language.
The thing I had in mind is that AFAIK sigils were over-used.
Syntax bikesheding: I am deeply unhappy about impl Trait
notation, because using a keyword (bold font in an editor) to name a type is way too loud. Remember C's struct
and Stroustroup loud syntax observation(slide 14)?
In https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761, @konstin suggested <Trait>
syntax. It looks really nice, especially in the input positions:
fn take_iterator(iterator: <Iterator<Item=i32>>)
I see that it will somewhat conflict with UFCS, but maybe this can be worked out?
I too feel using angle brackets instead of impl Trait to be a better choice, at least in return type position, e.g.:
fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}
<Trait>
syntax conflicts with generics, consider:
Vec<<FnOnce() -> bool>>
vs Vec<@FnOnce() -> bool>
If Vec<FnOnce() -> bool>
is allowed, then <Trait>
is good idea, it signifies the equivalence to generic type parameter. But since Box<Trait>
is different to Box<@Trait>
, have to give up <Trait>
syntax.
I prefer the impl
keyword syntax because when you read documentation rapidly this allow less way to misread prototypes.
What do you think ?
I'm just realising I did propose a superset to this rfc in the internals thread (Thanks for @matklad for pointing me here):
Allow traits in function parameters and return types to be used by surrounding them with angle brackets like in the following example:
fn transform(iter: <Iterator>) -> <Iterator> {
// ...
}
The compiler would then monomorphise the parameter using the same rules currently applied to generics. The return type could e.g. be derived from the functions implementation. This does mean that you can't simply call this method on a Box<Trait_with_transform>
or using it on dynamically dispatched objects in general, but it would still make the rules more permissive. I haven't read through all of the RFC discussion, so maybe there's a better solution already there I've missed.
I prefer the impl keyword syntax because when you read documentation rapidly this allow less way to missread prototypes.
A different color in the syntax highlighting should do the trick.
This paper by Stroustrup discusses similar syntactic choices for C++ concepts in section 7: http://www.stroustrup.com/good_concepts.pdf
Do not use the same syntax for generics and existentials. They are not the same thing. Generics allow the caller to decide what the concrete type is, while (this restricted subset of) existentials allows the function being called to decide what the concrete type is. This example:
fn transform(iter: <Iterator>) -> <Iterator>
should either be equivalent to this
fn transform<T: Iterator, U: Iterator>(iter: T) -> U
or it should be equivalent to this
fn transform(iter: impl Iterator) -> impl Iterator
The last example won't compile correctly, even on nightly, and it's not actually callable with the iterator trait, but a trait like FromIter
would allow the caller to construct an instance and pass it to the function without being able to determine the concrete type of what they're passing.
Maybe the syntax should be similar, but it should not be the same.
No need to distinguish any of (generics) or some of (existentials) in type name, it depends on where the type is used. When used in variables, arguments and struct fields always accept any of T, when used in fn return type always get some of T.
Type
, &Type
, Box<Type>
for concrete data types, static dispatch@Trait
, &@Trait
, Box<@Trait>
and generic type parameter for abstract data type, static dispatch&Trait
, Box<Trait>
for abstract data type, dynamic dispatchfn func(x: @Trait)
is equivalent to fn func<T: Trait>(x: T)
.
fn func<T1: Trait, T2: Trait>(x: T1, y: T2)
can be simply written as fn func(x: @Trait, y: @Trait)
.
T
paramter is still needed in fn func<T: Trait>(x: T, y: T)
.
struct Foo { field: @Trait }
is equivalent to struct Foo<T: Trait> { field: T }
.
When used in variables, arguments and struct fields always accept any of T, when used in fn return type always get some of T.
You can return any-of-Trait, right now, in stable Rust, using the existing generic syntax. It's a very heavily used feature. serde_json::de::from_slice
takes &[u8]
as a parameter and returns T where T: Deserialize
.
You can also meaningfully return some-of-Trait, and that's the feature we're discussing. You can not use existentials for the deserialize function, just like you can't use generics to return unboxed closures. They're different features.
For a more familiar example, Iterator::collect
can return any T where T: FromIterator<Self::Item>
, implying my preferred notation: fn collect(self) -> any FromIterator<Self::Item>
.
How about the syntax
fn foo () -> _ : Trait { ... }
for return values and
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
for parameters?
To me really none of the new suggestions come close to impl Trait
in it's elegancy. impl
is a keyword already known to every rust programmer and since it's used for implementing traits it actually suggests what the feature is doing just on its own.
Yeah, sticking with existing keywords seems ideal to me; I'd like to see impl
for existentials and for
for universals.
I am personally opposed to having
any
andsome
conflated into one keyword/sigil
@eddyb I wouldn’t consider it a conflation. It follows naturally from the rule:
((∃ T . F⟨T⟩) → R) → ∀ T . (F⟨T⟩ → R)
Edit: it’s one-way, not an isomorphism.
Unrelated: Is there any related proposal to also allow impl Trait
in other covariant positions such as
fn foo<F, R>(callback: F) -> R
where F: FnOnce(impl SomeTrait) -> R {
callback(create_something())
}
Right now, this is not a necessary feature, since you can always put a concrete time for impl SomeTrait
, which hurts readability but is otherwise not a big deal.
But if RFC 1522 feature stabilizes, then it would be impossible to assign a type signature to programs such the above if create_something
results in impl SomeTrait
(at least without boxing it). I think this is problematic.
@Rufflewind In the real world, things aren't so clear-cut, and this feature is a very specific brand of existentials (Rust has several by now).
But even then, all you have there is the use of covariance to determine what impl Trait
means inside and outside function arguments.
That's not enough for:
any
and some
are equally desirable)@Rufflewind That seems like the wrong bracketing for what impl Trait
is. I know Haskell exploits this relationship to use only the forall
keyword to represent both universals and existentials, but it doesn't work out in the context we're discussing.
Take this definition, for example:
fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }
If we use the rule that "impl
in arguments is universal, impl
in return types is existential", then the type of the foo
function item type is logically this (in made-up type notation):
forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))
Naively treating impl
as technically only meaning universal or only meaning existential and letting the logic work itself out doesn't actually work out. You would get either this:
forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)
Or this:
exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)
And neither of these reduce to what we want by logical rules. So ultimately any
/some
do capture an important distinction you can't capture with a single keyword. There are even reasonable examples in std
where you want universals in return position. For instance, this Iterator
method:
fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;
And there is no way to write it with impl
and the argument/return rule.
tl;dr having impl
contextually denote either universal or existential really does give it two distinct meanings.
For reference, in my notation the forall/exists relationship @Rufflewind mentioned looks like:
fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)
Which is related to the concept of trait objects (existentials) being equivalent to generics (universals), but not to this impl Trait
question.
That said, I'm not strongly in favour of any
/some
anymore. I wanted to be precise about what we're talking about, and any
/some
have this theoretical and visual niceness, but I would be fine with using impl
with the contextual rule. I think it covers all the common cases, it avoids contextual keyword grammar issues, and we can drop to named type parameters for the rest.
On that note, to match the full generality of universals, I think we'll eventually need a syntax for named existentials, which enables arbitrary where clauses and the ability to use the same existential in multiple places in the signature.
In summary, I'd be happy with:
impl Trait
as the shorthand for both universals and existentials (contextually).Naively treating impl as technically only meaning universal or only meaning existential and letting the logic work itself out doesn't actually work out. You would get either this:
@solson To me, a “naive” translation would result in the existential quantifiers right next to the type being quantified. Hence
(impl MyTrait)
is just syntactic sugar for
(exists<T: MyTrait> T)
which is a simple local transformation. Thus, a naive translation obeying the “impl
is always an existential” rule would result in:
fn(exists<T: ArgTrait> T) -> (exists<R: ReturnTrait> R)
Then, if you pull the quantifier out out of the function argument, it becomes
for<T: ArgTrait> fn(T) -> (exists<R: ReturnTrait> R)
So even though T
is always existential relative to itself, it appears as universal relative to the whole function type.
IMO, I think impl
may as well become the de facto keyword for existential types. In the future, perhaps one could conceivably construct more complicated existential types like:
(impl<T: MyTrait> (Vec<T>, T))
in analogy to universal types (via HRTB)
(for<'a> FnOnce(&'a T))
@Rufflewind That view doesn't work because fn(T) -> (exists<R: ReturnTrait>(R))
isn't logically equivalent to exists<R: ReturnTrait>(fn(T) -> R)
, which is what return-type impl Trait
really means.
(At least not in the constructive logic usually applied to type systems, where the specific witness chosen for an existential is relevant. The former implies the function could choose different types to return based on, say, the arguments, while the latter implies there is one specific type for all invocations of the function, as is the case in impl Trait
.)
I feel that we are getting a bit far afield, as well. I think contextual impl
is an okay compromise to make, and I don't think reaching for this kind of justification is necessary or particularly helpful (we certainly wouldn't teach the rule in terms of this kind of logical connection).
@solson Yeah you’re right: existentials can’t be floated out. This one does not hold in general:
(T → ∃R. f(R)) ⥇ ∃R. T → f(R)
whereas these do hold in general:
(∃R. T → f(R)) → T → ∃R. f(R)
(∀A. g(A) → T) ↔ ((∃A. g(A)) → T)
The last one is responsible for the re-interpretation of existentials in arguments as generics.
Edit: Oops, (∀A. g(A) → T) → (∃A. g(A)) → T
does hold.
I've posted an RFC with a detailed proposal to expand and stabilize impl Trait
. It draws on a lot of the discussion on this and earlier threads.
Worth noting that https://github.com/rust-lang/rfcs/pull/1951 has been accepted.
What's the status on this currently? We have an RFC that landed, we have people using the initial implementation, but I'm not clear on what items are todo.
It was found in #43869 that -> impl Trait
function does not support a purely diverging body:
fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
unimplemented!()
}
Is this expected (since !
does not impl Iterator
), or considered a bug?
What about defining inferred types, that could not only be used as return values, but as anything(i guess) a type can be used for currently?
Something like:
type Foo: FnOnce() -> f32 = #[infer];
Or with a keyword:
infer Foo: FnOnce() -> f32;
The type Foo
could then be used as a return type, parameter type or anything else a type can be used for, but it would be illegal to use it on two different places that require a different type, even if that type implements FnOnce() -> f32
in both cases. For example, the following would not compile:
infer Foo: FnOnce() -> f32;
fn return_closure() -> Foo {
|| 0.1
}
fn return_closure2() -> Foo {
|| 0.2
}
fn main() {
println!("{:?}, {:?}", return_closure()(), return_closure2()());
}
This shouldn't compile because even tho the return types from return_closure
and return_closure2
are both FnOnce() -> f32
, their types are actually different, because no two closures have the same type in Rust. For the above to compile you would thus need to define two different inferred types:
infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line
fn return_closure() -> Foo {
|| 0.1
}
fn return_closure2() -> Foo2 { //Changed Foo to Foo2
|| 0.2
}
fn main() {
println!("{:?}, {:?}", return_closure()(), return_closure2()());
}
I think what's happening here is quite obvious after seeing the code, even if you didn't know beforehand what the infer keyword does, and it is very flexible.
The infer keyword (or macro) would essentially tell the compiler to figure out what the type is, based on where it is used. If the compiler is not able to infer the type, it would throw an error, this could happen when there is not enough information to narrow down what type it has to be(if the inferred type isn't used anywhere, for example, although maybe it is better to make that specific case a warning), or when it is impossible to find a type that fits everywhere it is used(like in the example above).
@alvitawa See https://github.com/rust-lang/rfcs/pull/2071
@cramertj Ahh so that's why this issue had gotten so silent..
So, @cramertj was asking me about how I thought it would be best to resolve the problem of late-bound regions that they encountered in their PR. My take is that we probably want to "retool" our implementation a bit to try and look forward to the anonymous type Foo
model.
For context, the idea is roughly that
fn foo<'a, 'b, T, U>() -> impl Debug + 'a
would be (sort of) desugared to something like this
anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>
Note that in this form, you can see which generic parameters are captured because they appear as arguments to Foo
-- notably, 'b
is not captured, because it does not appear in the trait reference in any way, but the type parameters T
and U
always are.
Anyway, at present in the compiler, when you have a impl Debug
reference, we create a def-id that -- effectively -- represents this anonymous type. Then we have the generics_of
query, which computes its generic parameters. Right now, this returns the same as the "enclosing" context -- that is, the function foo
. This is what we want to change.
On the "other side", that is, in the signature of foo
, we represent impl Foo
as a TyAnon
. This is basically right -- the TyAnon
represents the reference to Foo
that we see in the desugaring above. But the way that we get the "substs" for this type is to use the "identity" function, which is clearly wrong -- or at least doesn't generalize.
So in particular there is a kind of "namespace violation" taking place here. When we generate the "identity" substs for an item, that normally gives us the substitutions we would use when type-checking that item -- that is, with all its generic parameters in scope. But in this case, we creating the reference to Foo
that appears inside of the function foo()
, and so we want to have the generic parameters of foo()
appearing in Substs
, not those of Foo
. This happens to work because right now those are one and the same, but it's not really right.
I think what we should be doing is something like this:
First, when we compute the generic type parameters of Foo
(that is, the anonymous type itself), we would begin constructing a fresh set of generics. Naturally it would include the types. But for lifetimes, we would walk over the trait bounds and identify each of the regions that appear within. That is very similar to this existing code that cramertj wrote, except we don't want to accumulate def-ids, because not all regions in scope have def-ids.
I think what we want to be doing is accumulating the set of regions that appear and putting them in some order, and also tracking the values for those regions from the point-of-view of foo()
. It's a bit annoying to do this, because we don't have a uniform data structure that represents a logical region. (We used to have the notion of FreeRegion
, which would almost have worked, but we don't use FreeRegion
for early-bound stuff anymore, only for late-bound stuff.)
Perhaps the easiest and best option would be to just use a Region<'tcx>
, but you'd have to shift the debruijn index depths as you go to "cancel out" any binders that got introduced. This is perhaps the best choice though.
So basically as we get callbacks in visit_lifetime
, we would transform those into a Region<'tcx>
expressed in the initial depth (we'll have to track as we pass through binders). We'll accumulate those into a vector, eliminating duplicates.
When we're done, we have two things:
RegionParameterDef
data structures...).OK, sorry if that is a cryptic. I can't quite figure out how to say it more clearly. Something I'm not sure of though -- right now, I feel that our handling of regions is pretty complex, so maybe there is a way to refactor things to make it more uniform? I would bet $10 that @eddyb has some thoughts here. ;)
@nikomatsakis I believe a lot of that is similar to what I've told @cramertj, but more fleshed out!
I've been thinking about existential impl Trait
and I encountered a curious case where I think we should proceed with caution. Consider this function:
trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
()
}
As you can validate on play, this code compiles today. However, if we dig into what is happening, it highlights something that has a "fowards compatibility" danger that concerns me.
Specifically, it's clear how we deduce the type that is being returned here (()
). It's less clear how we deduce the type of the impl Debug
parameter. That is, you can think of this return value as being something like -> ?T
where ?T: Foo<?U>
. We have to deduce the values of ?T
and ?U
based just on the fact that ?T = ()
.
Right now, we do this by leveraging the fact that there exists only one impl. However, this is a fragile property. If a new impl is added, the code will no longer compile, because now we cannot uniquely determine what ?U
must be.
This can happen in lots of scenarios in Rust -- which is concerning enough, but orthogonal -- but there is something different about the impl Trait
case. In the case of impl Trait
, we don't have a way for user's to add type annotations to guide the inference along! Nor do we really have a plan for such a way. The only solution is to change the fn interface to impl Foo<()>
or something else explicit.
In the future, using abstract type
, one could imagine allowing users to explicitly give the hidden value (or perhaps just incomplete hints, using _
), which could then help inference along, while keeping roughly the same public interface
abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
()
}
Still, I think it would be prudent to avoid stabilizing "nested" uses of existential impl Trait, except for in associated type bindings (e.g., impl Iterator<Item = impl Debug>
does not suffer from these ambiguities).
In the case of impl Trait, we don't have a way for user's to add type annotations to guide the inference along! Nor do we really have a plan for such a way.
Perhaps it could look like UFCS? e.g. <() as Foo<()>>
-- not changing the type like a bare as
, just disambiguating it. This is currently invalid syntax as it expects ::
and more to follow.
I just found an interesting case regarding type inference with impl Trait for Fn
:
The following code compiles just fine:
fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
match s {
"+" => ::std::ops::Add::add,
"-" => ::std::ops::Sub::sub,
"<" => |a,b| (a < b) as i32,
_ => unimplemented!(),
}
}
If we comment out the Sub-line, a compile error is thrown:
error[E0308]: match arms have incompatible types
--> src/main.rs:4:5
|
4 | / match s {
5 | | "+" => ::std::ops::Add::add,
6 | | // "-" => ::std::ops::Sub::sub,
7 | | "<" => |a,b| (a < b) as i32,
8 | | _ => unimplemented!(),
9 | | }
| |_____^ expected fn item, found closure
|
= note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
--> src/main.rs:7:16
|
7 | "<" => |a,b| (a < b) as i32,
| ^^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
@oberien This doesn't seem related to impl Trait
-- it's true of inference in general. Try this slight modification of your example:
fn main() {
let _: i32 = (match "" {
"+" => ::std::ops::Add::add,
//"-" => ::std::ops::Sub::sub,
"<" => |a,b| (a < b) as i32,
_ => unimplemented!(),
})(5, 5);
}
Looks like this is closed now:
ICEs when interacting with elision
One thing that I don't see listed in this issue or in the discussion is the ability to store closures and generators – that aren't provided by the caller – in struct fields. Right now, this is possible but it looks ugly: you have to add a type parameter to the struct for each closure/generator field, and then in the constructor function's signature, replace that type parameter with impl FnMut/impl Generator
. Here is an example, and it works, which is pretty cool! But it leaves a lot to be desired. It would be way better if you could get rid of the type parameter:
struct Counter(impl Generator<Yield=i32, Return=!>);
impl Counter {
fn new() -> Counter {
Counter(|| {
let mut x: i32 = 0;
loop {
yield x;
x += 1;
}
})
}
}
impl Trait
may not be the right way to do this – probably abstract types, if I've read and understood RFC 2071 correctly. What we need is something that we can write in the struct definition so that the actual type ([generator@src/main.rs:15:17: 21:10 _]
) can be inferred.
@mikeyhew abstract types would indeed be the way we expect that to work, I believe. The syntax would look roughly like
abstract type MyGenerator: Generator<Yield = i32, Return = !>;
pub struct Counter(MyGenerator);
impl Counter {
pub fn new() -> Counter {
Counter(|| {
let mut x: i32 = 0;
loop {
yield x;
x += 1;
}
})
}
}
Is there a fallback path if it's someone else's impl Generator
that I want to put in my struct, but they didn't make an abstract type
for me to use?
@scottmcm You can still declare your own abstract type
:
// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }
// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;
pub struct Counter(MyGenerator);
impl Counter {
pub fn new() -> Counter {
let inner: MyGenerator = foo();
Counter(inner)
}
}
@cramertj Wait, abstract types are already in nightly?! Where's the PR?
@alexreg No, they are not.
Edit: Greetings, visitors from the future! The issue below has been resolved.
I'd like to call attention to this funky edge case of usage that appears in #47348
use ::std::ops::Sub;
fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }
Should returning a projection on impl Trait
like this even be allowed? (because currently, it is.)
I couldn't locate any discussion about usage like this, nor could I find any test cases for it.
@ExpHP Hmm. It does seem problematic, for the same reason that impl Foo<impl Bar>
is problematic. Basically, we don't have any real constraint on the type in question -- only on the things projected out from it.
I think we want to reuse the logic around "constrained type parameters" from impls. In short, specifying the return type should "constrain" the impl Sub
. The function I am referring to is this one:
Tiny bit of triage for people who like checkboxes:
@rfcbot fcp merge
I propose that we stabilize the conservative_impl_trait
and universal_impl_trait
features, with one pending change (a fix to https://github.com/rust-lang/rust/issues/46541).
The tests for these features can be found in the following directories:
run-pass/impl-trait ui/impl-trait compile-fail/impl-trait
The details of parsing of impl Trait
were resolved in RFC 2250 and implemented in https://github.com/rust-lang/rust/pull/45294.
impl Trait
has been banned from nested-non-associated-type position and certain qualified path positions in order to prevent ambiguity. This was implemented in https://github.com/rust-lang/rust/pull/48084.
After this stabilization, it will be possible to use impl Trait
in argument position and return position of non-trait functions. However, the use of impl Trait
anywhere in Fn
syntax is still disallowed in order to allow for future design iteration. Additionally, manually specifying the type parameters of functions which use impl Trait
in argument position is not allowed.
Team member @cramertj has proposed to merge this. The next step is review by the rest of the tagged teams:
No concerns currently listed.
Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.
NEW TRACKING ISSUE = https://github.com/rust-lang/rust/issues/63066
Implementation status
The basic feature as specified in RFC 1522 is implemented, however there have been revisions that are still in need of work:
let x: impl Trait
static
andconst T: impl Trait
abstract type
RFCs
There have been a number of RFCs regarding impl trait, all of which are tracked by this central tracking issue.
abstract type
in modules and implslet
,const
, andstatic
positionsimpl Trait
anddyn Trait
with multiple boundsUnresolved questions
The implementation has raised a number of interesting questions as well:
impl
keyword when parsing types? Discussion: 1Send
forwhere F: Fn() -> impl Foo + Send
?impl Trait
after->
infn
types or parentheses sugar? #45994fn foo<T>(x: impl Iterator<Item = T>>)
?impl Trait
as arguments in the list, permitting migrationexistential type Foo: Bar
ortype Foo = impl Bar
? (see here for discussion)existential type
in an impl be just items of the impl, or include nested items within the impl functions etc? (see here for example)