Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upAuto-generated sum types #2414
Comments
Centril
added
the
T-lang
label
Apr 23, 2018
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Apr 23, 2018
|
Thanks for creating a separate issue for this. To be honest, I'm not sure we need explicit syntax for this. It's more of an (important) implementation detail than a user-facing feature, no? But if one does want to make the syntax explicit, then I suggest putting something like |
This comment has been minimized.
This comment has been minimized.
@alexreg one reason to make it explicit is it does have performance implications, each time you call a method there will have to be a branch to call into the current type (hmm, unless these were implemented as some form of stack-box with a vtable instead, either way that still changes the performance). My first thought was also to make it a modifier on the |
This comment has been minimized.
This comment has been minimized.
|
I might be wrong, but wouldn't the |
This comment has been minimized.
This comment has been minimized.
|
@Pauan Oh indeed, I was thinking @Nemo157, @alexreg The issue with putting this as a modifier in the type signature is the fact it wouldn't work well inside a function: fn bar() -> Option<LinkedList<char>> { /* ... */ }
// This is allowed
fn foo() -> impl enum Iterator<Item = char> {
match bar() {
Some(x) => x.iter(),
None => "".iter(),
}
}
// Either this is not allowed, or the loss in performance is not explicit
fn foo() -> impl enum Iterator<Item = char> {
let mut tmp = match bar() {
Some(x) => x.iter(),
None => "".iter(),
};
let n = tmp.next();
match n {
Some(_) => tmp,
None => "foo bar".iter(),
}
} |
This comment has been minimized.
This comment has been minimized.
|
Haven't you just invented dynamic dispatch?? |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Apr 23, 2018
|
Yeah, fair point about the performance hit. It’s a small one, but it wouldn’t be in the spirit of Rust to hide it from the user syntactically. |
This comment has been minimized.
This comment has been minimized.
|
@Ekleog you can use the exact same syntax for inside a function, somewhere you need to mention the trait that you're generating a sum type for anyway: fn foo() -> impl enum Iterator<Item = char> {
let mut tmp: impl enum Iterator<Item = char> = match bar() {
Some(x) => x.iter(),
None => "".iter(),
};
let n = tmp.next();
match n {
Some(_) => tmp,
None => "foo bar".iter(),
}
}@est31 a constrained form of dynamic dispatch that could potentially get statically optimized if the compiler can prove only one or the other case is hit. Or, as I briefly mentioned above it could be possible for this to be done via a union for storage + a vtable for implementation, giving the benefits of dynamic dispatch without having to use the heap. (Although, if you have wildly different sizes for the different variants then you pay the cost in always using the size of the largest.) One thing that I think might be important is to benchmark this versus just boxing and potentially have a lint recommending switching to a box if you have a large number of variants (I'm almost certain that a 200 variant switch would be a lot slower than dynamically dispatching to one of 200 implementors of a trait, but I couldn't begin to guess at what point the two crossover in call overhead, and there's the overhead of allocating the box in the first place). |
This comment has been minimized.
This comment has been minimized.
|
@Nemo157 Thanks for explaining things better than I could! I'd just have a small remark about your statement: I don't think a 200-variant switch would be a lot slower than dynamic dispatch: the switch should be codegen'd as a jump table, which would give something like (last time I wrote assembler is getting a bit long ago so I'm not sure about the exact syntax) So the mere number of implementors shouldn't matter much in evaluating the performance of this dispatch vs. a box. The way of using them does have an impact, but this will likely be hard to evaluate from the compiler's perspective. However, what may raise an issue about performance is nesting of such sum types: if you have a sum type of a sum type of etc., then you're going to lose quite a bit of time going through all these jump tables. But the compiler may detect that one member of the sum type is another sum type and just flatten the result, so I guess that's more a matter of implementation than specifiction? :) |
This comment has been minimized.
This comment has been minimized.
LLVM is capable of doing devirtualisation.
That's a point admittedly. Dynamically sized stack objects are a possibility but they have certain performance disadvantages. |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Apr 23, 2018
|
Is there anything wrong with the bike shed color I think procedural macros could generate this right now. If coersions develop further then maybe doing so would becomes quite simple even. Right now, there are |
This comment has been minimized.
This comment has been minimized.
|
So if we are going to start painting the bike shed (there seems to be little opposition right now, even though it has only been like a day since initial posting), I think there is a first question to answer: Should the indication of the fact the return is an anonymous sum type lie in the return type or at the return sites?i.e. fn foo(x: T) -> MARKER Trait {
match x {
Bar => bar(),
Baz => baz(),
Quux => quux(),
}
}
// vs.
fn foo(x: T) -> impl Trait {
match x {
Bar => MARKER bar(),
Baz => MARKER baz(),
Quux => MARKER quux(),
}
}Once this question will have been answered, we'll be able to think about the specifics of what |
This comment has been minimized.
This comment has been minimized.
|
So, now, my opinion: I think the fact the return is an anonymous sum type should lie at the return site, for two reasons:
On the other hand, the only argument I could think of in favor of putting the marker in the return type is that it makes for less boilerplate, but I'm not really convinced, so I'm maybe not pushing forward the best arguments. |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Apr 24, 2018
|
@Ekleog Yeah, I'm with you on return site actually, even though I proposed the return type syntax above. As you say, it reflects the fact that it's more of an implementation detail that consumers of the function don't need to (shouldn't) care about. Also, I think the analogy to |
This comment has been minimized.
This comment has been minimized.
For a closed set of traits, sure, but to allow this to be used for any trait requires compiler support for getting the methods of the traits. Delegation plus some of its extensions might enable this to be fully implemented as a procedural macro. I'm tempted to try and write a library or procedural macro version of this, I am currently manually doing this for |
This comment has been minimized.
This comment has been minimized.
|
Various forms of
I believe the main thing preventing these discussions from going anywhere is that making it even easier to use impl Trait further accentuates the only serious problem with impl Trait: that it encourages making types unnameable. But now that the @Nemo157 Hmm, what delegation extensions do you think we'd need? The "desugaring" I've always imagined is just a Though I think we should probably not block this on delegation, since it could just be compiler magic. |
This comment has been minimized.
This comment has been minimized.
|
From the current delegation RFC the extensions required are "delegating for an enum where every variant's data type implements the same trait" (or "Getter Methods" + "Delegate block") and "Delegating 'multiple Self arguments' for traits like PartialOrd" (although, this could be implemented without it and this feature would be in the same state as normal delegation until it's supported). One thing I just realised is that delegation won't help with unbound associated types, required to support use cases like: #[enumified]
fn foo() -> impl IntoIterator<Item = u32> {
if true {
vec![1, 2]
} else {
static values: &[u32] = &[3, 4];
values.iter().cloned()
}
}would need to generate something like enum Enumified_foo_IntoIterator {
A(Vec<u32>),
B(iter::Cloned<slice::Iter<'static>>),
}
enum Enumified_foo_IntoIterator_IntoIter_Iterator {
A(vec::Iter<u32>),
B(iter::Cloned<slice::Iter<'static>>),
}
impl IntoIterator for Enumified_foo {
type Item = u32;
type IntoIter = Enumified_foo_IntoIterator_IntoIter_Iterator;
fn into_iter(self) -> self::IntoIter {
match self {
Enumified_foo_IntoIterator::A(a)
=> Enumified_foo_IntoIterator_IntoIter_Iterator::A(a.into_iter()),
Enumified_foo_IntoIterator::B(b)
=> Enumified_foo_IntoIterator_IntoIter_Iterator::B(b.into_iter()),
}
}
}
impl Iterator for Enumified_foo_IntoIterator_IntoIter_Iterator {
...
} |
This comment has been minimized.
This comment has been minimized.
|
@Ixrec Oh indeed, I didn't think of the However, this could be “fixed” by piping This wouldn't change the behaviour for existing code (as adding So I still think that the ease of using However, as a downside of fn bar() -> bool {…}
struct Baz {} fn baz() -> Baz {…}
struct Quux {} fn quux() -> Quux {…}
struct More {} fn more() -> More {…}
trait Trait1 {} impl Trait1 for Baz {} impl Trait1 for Quux {} impl Trait1 for More
trait Trait2 {} impl Trait2 for Baz {} impl Trait2 for Quux {}
fn foo() -> impl Trait1 {
let x = match bar() {
true => MARKER baz(), //: TypeInProgress(Baz)
false => MARKER quux(), //: TypeInProgress(Quux)
}; //: TypeInProgress(Baz, Baz) & TypeInProgress(Quux, Quux)
// = TypeInProgress(Trait1 + Trait2, enum { Baz, Quux })
// (all the traits implemented by both)
if bar() {
MARKER x //: TypeInProgress(Trait1 + Trait2, enum { Baz, Quux })
} else {
MARKER more() //: TypeInProgress(More, More)
} // TypeInProgress(Trait1 + Trait2, enum { Baz, Quux }) & TypeInProgress(More, More)
// = TypeInProgress(Trait1, enum { Baz, Quux, More })
// (all the types implemented by both)
}And, once this forward-running phase has been performed, the actual type can be observed (ie. here On the other hand, for a return-site-level // (skipping the same boilerplate)
fn foo() -> MARKER Trait1 {
let x: MARKER Trait1 = match bar() {
true => baz(),
false => quux(),
}; // Here we need to infer MARKER Trait1.
// Observing all the values that come in, we see it must be enum { Baz, Quux }
if bar() {
x
} else {
more()
} // Here we need to infer MARKER Trait1.
// Observing all the values that come in, it must be enum { enum { Baz, Quux }, More }
}I personally find the syntax of the second example less convenient (it forces writing down exactly which trait(s) we want to have, not letting type inference do its job) and the end-result less clean (two nested What do you think about the idea of having |
This comment has been minimized.
This comment has been minimized.
For me, the primary use case is returning an I'm not sure I understand the sentiment behind "not letting type inference do its job". Both variations of this idea involve us writing an explicit MARKER to say we want an autogenerated anonymous enum type. In both cases, type inference is only gathering up all the variants for us, and never inferring the need for an anon enum type in the first place. In both cases, the variable
I'm not sure I buy either of these claims. To the extent that "detecting common subtrees" is important, I would expect the existing enum layout optimizations to effectively take care of that for free. We probably need an actual compiler dev to comment here, but my expectation would be that the actual optimization-inhibiting difficulties would come from having "all traits" implemented by the anon enums, instead of just the traits you need. And to me, the autogenerated anonymous enum type implementing more traits than I need/want it to is "less clean". I guess that's one of those loaded terms that's not super helpful. I'm not seeing the significance of the
I think "the idea of having ? return MARKER x.unwrap_err()" is also strictly an implementation detail that's not really relevant to the surface syntax debate, especially since ? is already more than just sugar over a macro. To clarify, I believe the real, interesting issue here is whether we want these anonymous enum types to implement only the traits we explicitly ask for, or all the traits they possibly could implement. Now that this question has been raised, I believe it's the only outstanding issue that really needs to get debated to make a decision on whether MARKER goes at every return site or only once in the signature/binding. My preference is of course for the traits to be listed explicitly, since I believe the primary use case to be function signatures where you have to list them explicitly anyway, and I also suspect that auto-implementing every possible trait could lead to unexpected type inference nuisances, or runtime behavior, though I haven't thought about that much. Let's make the type inference nuisance thing concrete. Say Trait1 and Trait2 both have a foo method, and types A and B both implement both traits. Then you want to write a function that, as in your last two examples, returns |
This comment has been minimized.
This comment has been minimized.
Well, I added it to answer your concern that it would be painful to have to add
That's true. However, the same could be said with regular types: if I return a single value (so no
Well, apart from the end-result being cleaner (and I don't think Actually, I'd guess that's how fn foo() -> Vec<u8> {
let res = Vec::new; //: TypeInProgress(Vec<_>)
bar();
res // Here we know it must be Vec<u8>, so the _ from above is turned into u8
}
Completely agree with you on this point :) |
This comment has been minimized.
This comment has been minimized.
nielsle
commented
May 2, 2018
•
|
Would it be practical to use a procedural macro to derive a specialized iterator for each word? (It seems possible, but a little verbose) #[derive(IntoLetterIter)]
#[IntoLetterIterString="foo"]
struct Foo;
#[derive(IntoLetterIter)]
#[IntoLetterIterString="hello"]
struct Hello;
fn foo(x: bool) -> impl IntoIterator<Item = u8> {
if x {
Foo
} else {
Hello
}
} |
This comment has been minimized.
This comment has been minimized.
|
I'm concerned with the degree to which this seems to combine the implementation details of this specific optimization with the code wanting to use that optimization. It seems like, despite I also wonder to what degree we could detect the cases where this makes sense (e.g. cases where we can know statically which impl gets returned) and handle those without needing the hint. If the compiler is already considering inlining a function, and it can see that the call to the function will always result in the same type implementing the Trait, then what prevents it from devirtualizing already? I'd suggest, if we want to go this route, that we need 1) an implementation of this that doesn't require compiler changes, such as via a macro, 2) benchmarks, and 3) some clear indication that we can't already do this with automatic optimization. And even if we do end up deciding to do this, I'd expect it to look less like a marker on the return type or on the return expressions, and more like an |
This comment has been minimized.
This comment has been minimized.
maplant
commented
May 2, 2018
•
|
Just to add my thoughts to this without clutter, here is my version of the optimization: https://internals.rust-lang.org/t/allowing-multiple-disparate-return-types-in-impl-trait-using-unions/7439 I think that automatic sum type generation should be left to procedural macros |
This comment has been minimized.
This comment has been minimized.
|
@joshtriplett I don’t believe the only reason to want this is as an optimisation. One of the major reasons I want this is to support returning different implementations of an interface based on runtime decisions without requiring heap allocation, for use on embedded devices. I have been able to avoid needing this by sticking to compile time decisions (via generics) and having a few manually implemented delegating enums, but if this were supported via the language/a macro somehow that would really expand the possible design space. I do agree that experimenting with a macro (limited to a supported set of traits, since it’s impossible for the macro to get the trait method list) would be the way to start. I’ve been meaning to try and throw something together myself, but haven’t found the time yet. |
This comment has been minimized.
This comment has been minimized.
maplant
commented
May 2, 2018
|
@joshtriplett to address part of your comment, i.e. benchmarks, I created a repository that uses my method and benchmarks it against Box. Although I only have one test case and it is somewhat naive, it seems that my method is about twice as fast as Box. Repo here: https://github.com/DataAnalysisCosby/impl-trait-opt |
This comment has been minimized.
This comment has been minimized.
|
@Nemo157 I don't think you need heap allocation to use But in any case, I would hope that if it's available as an optimization hint, it would have an |
This comment has been minimized.
This comment has been minimized.
|
@joshtriplett Let's look at this example (here showing what we want to do): trait Trait {}
struct Foo {} impl Trait for Foo {}
struct Bar {} impl Trait for Bar {}
fn foo(x: bool) -> impl Trait {
if x {
Foo {}
} else {
Bar {}
}
}This doesn't build. In order to make it build, I have a choice: either make it a heap-allocated object: fn foo(x: bool) -> Box<Trait> {
if x {
Box::new(Foo {})
} else {
Box::new(Bar {})
}
}Or I do it with an enum: enum FooBar { F(Foo), B(Bar) }
impl Trait for FooBar {}
fn foo(x: bool) -> impl Trait {
if x {
FooBar::F(Foo {})
} else {
FooBar::B(Bar {})
}
}The aim of this idea is to make the enum solution actually usable without a lot of boilerplate. Is there another way to do this without heap allocation that I'd have missed? As for the idea of making it an optimization, do you mean “just return a Box and have the compiler |
This comment has been minimized.
This comment has been minimized.
|
@Ekleog Ah, thank you for the clarification; I see what you're getting at now. |
This comment has been minimized.
This comment has been minimized.
nielsle
commented
May 3, 2018
•
|
Regarding the third playground example, you can use derive_more to derive AFAICS a procedural macro on the following form could potentially solve the complete problem #[derive(IntoLetterIter)]
enum FooBar {
#[format="foo"]
Foo,
#[format="hello"]
Hello,
} |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Sep 4, 2018
•
|
@Ekleog Maybe the two ideas could be called "marker at unification" and "marker at expression"? "marker at unification" requires writing the keyword only once, so it'd be more concise, but "marker at unification" should not be mistaken to mean "marker at return type". fn bar() -> Option<LinkedList<char>> { /* ... */ }
fn foo() -> impl Iterator<Item = char> {
impl enum match bar() {
Some(x) => x.iter(),
None => "".iter(),
}
}
fn foo() -> impl Iterator<Item = char> {
let mut tmp = impl enum match bar() { // first unification, of the match arms
Some(x) => x.iter(),
None => "".iter(),
};
let n = tmp.next();
impl enum match n { // second unification
Some(_) => tmp,
None => "foo bar".iter(),
} // this should not generate a 2-level nested enum as return type because it can be flattened
}With "marker at expression" it would be much more verbose, requiring the ("marker at return type" would only allow unification for return expressions but we want to allow it for expressions inside function bodies, too.) |
This comment has been minimized.
This comment has been minimized.
|
@Boscop I like your names :) however, I still am not convinced by marker-at-unification: how would you have it handle this example (from somewhere above in the thread)? There is no way to unify after the fn bar() -> bool { /* ... */ }
fn baz() -> Vec<Baz> { /* ... */ }
fn quux() -> Vec<Quux> { /* ... */ }
// Baz and Quux implement Trait
fn foo() -> Vec<impl Trait> {
if bar() {
baz().map(|x| marker x).collect()
} else {
quux().map(|x| marker x).collect()
}
} |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Sep 6, 2018
•
|
@Ekleog The location of the marker does not influence where the compiler actually unifies! But this shows that maybe the name "marker at unification" should be changed to "marker before branches" (aka "marker at sub-tree root"), because the actual unifying conversion always happens somewhere in the branches (in this case before the So the syntax is independent of the unification location, we just need to express which sub-tree should be unified. "marker before branches" (aka "marker at sub-tree root") would only require this keyword once (at the root of the sub-tree, before the (syntactical) branches), whereas "marker at expressions" (aka "marker inside branches" / "marker at sub-tree leaves") requires the keyword in every branch/sub-tree leaf.. With both syntaxes, the compiler would choose the same point for the actual unification: The last point where single values of type So to make the naming clearer, maybe we should just use "marker at tree root" vs "marker at leaves".. |
This comment has been minimized.
This comment has been minimized.
|
@Boscop The problem is that if I expect this to work with marker-at-tree-root: (just taking the previous example and moving the fn foo() -> Vec<marker impl Trait> {
if bar() {
baz().into_iter().map(|x| x).collect()
} else {
quux().into_iter().map(|x| x).collect()
}
}then I would also naturally expect this to work, because fn foo() -> Vec<marker impl Trait> {
if bar() {
baz().into_iter().collect()
} else {
quux().into_iter().collect()
}
}and then the fn foo() -> Vec<marker impl Trait> {
if bar() {
baz()
} else {
quux()
}
}at which point, if the compiler actually implements this in a way so that it works, there is an implicit reallocation. Hence my not really liking this option :) |
This comment has been minimized.
This comment has been minimized.
Intuitively, I do not expect Consider this simple example: let x: Vec<char> = baz();
let x: String = baz().into_iter().collect();As you can see, My understanding is that the auto-coercion of a value into the anonymous enum only applies to the base type. So a So I would expect that your final example will give a compile error, because it will refuse to unify In other words, this should also be a compile error: fn foo() -> Vec<impl Trait> {
if bar() {
marker baz()
} else {
marker quux()
}
} |
This comment has been minimized.
This comment has been minimized.
|
What you say about Also, even Also, the As for your last example about marker-at-leafs, we completely agree that this should not work, and it's why I had written |
This comment has been minimized.
This comment has been minimized.
That's probably true, yeah (at least without major compiler trickery), in which case regardless of whether it uses "marker at expression" or "marker at type", the code will be identical, the only difference is where the marker is placed. So the power of the two approaches should be identical, it's purely personal preference whether you prefer to explicitly mark where the coercion happens, or whether you prefer the convenience of only needing to specify the marker once. I don't have a strong preference either way, but perhaps I'm leaning very slightly toward "marker at expression", simply because it makes it more obvious that
That's not necessarily true, depending on the implementation of I agree that intuitively it feels like it should be a no-op (just like how
Great, I'm glad we're in agreement about that. |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Sep 16, 2018
•
|
@Ekleog @Pauan I think we already agreed that "marker at return type" is not what we want, because we also want to be able to unify sub-expression trees within a function body.
At least an implicit
Yes, if we agree that we don't want the compiler to insert a magic Btw, with "marker at expression", when we get fn foo() -> Vec<impl Trait> {
let iter: impl Iterator<Item = impl Trait> = if bar() {
baz().into_iter().map(|x| marker x)
} else {
quux().into_iter().map(|x| marker x)
};
iter.collect() // collect() after the unified expression tree
}And when we get fn foo() -> Vec<impl Trait> {
if bar() {
baz().into_iter().map(|x| marker x)
} else {
quux().into_iter().map(|x| marker x)
}.collect() // collect() after the unified expression tree
}so you wouldn't even have to write |
This comment has been minimized.
This comment has been minimized.
I don't see how "marker at type" prevents that. You can do stuff like this: fn foo() {
let x: marker impl Trait = ...;
}This will unify the various types within
I don't think I ever implied that. My proposal was that whenever converting an expression of type It just so happens that with Iterators the simplest way to create a My comment about "compiler trickery" was with regards to the compiler doing the automatic conversion without |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Sep 17, 2018
fn foo() {
let x: marker impl Trait = ...;
}@Pauan But we shouldn't require that the expression that we want to unify has to be bound with a
The compiler can't do it automatically for wrapper types without knowing how to unwrap and re-wrap them though! It would need special-cased "magic" which would make custom wrapper types second-class citizens.
But if we don't use "marker at expression" and then require people to write Can we agree that "marker at expression" would satisfy all our requirements? It works for sub-expressions in function bodies (not just return expressions), it doesn't require a (typed) let binding, and it makes it clear where the conversion happens! |
This comment has been minimized.
This comment has been minimized.
But that's already a requirement for "marker at expression": fn foo() {
let x: impl Trait = ... marker ...;
}The type is needed, because without the type annotation the Rust compiler doesn't know what trait you're trying to convert it into. The two systems are fundamentally equivalent, the only difference is the syntax (whether
Yes it can, very easily. Why do you say it can't?
Yes, which is the same as how closures are handled, but that's not a problem. To be clear, "marker at expression" also has that problem, you act like it's a problem only with "marker at type", but it's not. Regardless of which system is used, the compiler will generate an anonymous type, wrap the values into the type, and implement the trait for the type. They are exactly the same system, the only difference is the syntax for where the marker goes. There is no fundamental difference between them.
I agree, which is why I said in an earlier post that I'm very slightly in favor of "marker at expression". But even then, the differences between them are minor. Both systems have benefits and drawbacks.
Sure, but "marker at type" also satisfies the requirements.
This is irrelevant, because both systems work fine with sub-expressions.
Except that it does.
Yes, that is the primary benefit of it (which I already admitted to in an earlier post, I'm not sure why you keep trying to convince me of this). The primary downside is needing to specify It just occurred to me that there is a third option: have the marker at both the type and expressions. This is even more verbose, but it makes everything extremely explicit. |
This comment has been minimized.
This comment has been minimized.
Well, this is potentially not true: the compiler could infer
I think @Boscop was speaking of making
Well, a regular |
This comment has been minimized.
This comment has been minimized.
|
These examples involving containers or iterators of autogenerated enum types are starting to feel subtle enough that I'm not entirely sure it's worth going out of our way to support them. After all, whatever we end up using for |
This comment has been minimized.
This comment has been minimized.
I'm not sure if that's true. It seems to me that inferring trait usage is quite a lot different (and more complicated) than inferring types like
Oh, you're right, it does sound like they were saying that, my mistake. Though I had only mentioned compiler trickery in one small off-hand remark (and I wasn't in favor of it), and all of the discussions since then have been about requiring the When I said this:
I was not referring to some magic Iterator system, just an extremely simple type unification: when the compiler sees This works with any expression, anywhere (as long as it is being unified with And it certainly would not allow for things like
That is a good point, yeah.
I'm not sure why the conversation has veered so far into The two primary proposals are:
The differences are purely in the syntax (the type unification is the same, the anonymous type is the same, etc.) In either case Iterators do not get any sort of special treatment, they're the same as anything else. So you will need to use And the same applies to
This is the exploration / design phase, it's normal for things to get complicated and confusing, because many people are exploring many different possible options. The goal right now isn't to "decide" on some pristine final solution, the goal is to explore, and try out different ideas. In addition, people are trying to explain their ideas (and also clarify any misconceptions), and people also change their mind, or new proposals are added, etc. So, some messiness is unavoidable. However, that doesn't mean that the final sugar will be complicated or confusing. The two proposals right now are both quite simple (to understand, and also to implement). I expect that after the exploration phase, a proper RFC will be written, in a clear and understandable way, without any of the current confusion or complications. |
This comment has been minimized.
This comment has been minimized.
Sorry, I didn't mean to imply I thought anyone was proposing Iterator-specific magic, or that the on-paper description of the syntax had ever stopped being trivial. What I meant was that people are making arguments for what the syntax ought to be based on what would be less weird for
It's the need for |
This comment has been minimized.
This comment has been minimized.
|
@Ixrec The case for That said, my main argument in favor of marker-at-leaf-expressions still remains (well, after the original flip-flop) that it's how things would be done with a manually-written enum: just replace Now, I must also say that my original use case for this was for huge futures. With async/await moving forward, this use case will likely become a lot less relevant, so I'm no longer sure this should actually get syntactic sugar. Just to say that I won't be pushing this forward until we gain some experience with async/await, to know whether there is still a real need for this RFC (for me) after async/await. :) |
This comment has been minimized.
This comment has been minimized.
For (historical?) context, way way back when I first got into this topic and I suggested we call this So that's part of the reason the recent discussion of nested things like |
This comment has been minimized.
This comment has been minimized.
|
@Ixrec (aside, this is the first time I've noticed that's a capital I not a lowercase l) error types have some of the exact same issues with wanting to have the conversion happen inside closures though, for example you might want to write something like use std::error::Error;
fn foo() -> Result<u32, Error1> where Error1: Error { ... }
fn bar(baz: u32) -> Result<u64, Error2> where Error2: Error { ... }
fn qux() -> Result<u64, marker impl Error> {
let baz = foo().map_err(|e| marker e)?;
let result = bar(baz).map_err(|e| marker e)?;
Ok(result)
}to make the short way to write it work, somehow fn qux() -> Result<u64, marker impl Error> {
Ok(bar(foo()?)?)
} |
This comment has been minimized.
This comment has been minimized.
|
Yeah, in older discussions I think it was always assumed that if this feature happened |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Sep 20, 2018
•
|
@Ixrec Once we have @Ekleog @Pauan Yes, there was some confusion. I'm not in favor of special-casing unwrapping/rewrapping for Iterators or any other type, that's why I prefer "marker at expression" so that we'd write foo(if cond { marker a } else { marker b });
fn foo<T: Bar>(x: T) { ... } // or written as: fn foo(x: impl Bar) { ... }it allows the compiler to infer that the trait to use for unification is Even in cases where it can't be inferred, it should work with a Another argument for keeping the marker away from the But the strongest argument for "marker at expression" IMO is |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Sep 20, 2018
|
I like roughly the Also, there are seemingly many parallels to trait objects here, which we could leverage via improvements in DST handling:
In this, We might need procedural macros to interact with type inference for that exact syntax to work, as the different invocations of
You could still simplify this syntax with a convenience macro though:
|
This comment has been minimized.
This comment has been minimized.
|
@Boscop (disclaimer: I'm in favor of marker-at-expression too) IMO, there is still an argument in favor of marker-at-type, and it is the question of The answer to this question is highly non-obvious to me. @burdges The Also, the issues with using only the |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Sep 20, 2018
|
@Ekleog Btw, |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Sep 21, 2018
|
You first do not want We cannot have syntactic ambiguity between the first and second cases here of course since they create different associated types. I only suggested requiring identical associated types, which supports the first case but completely forbids this second case. We might call this second form "associated type erasure safe", especially in the trait object context. We have many object safe traits like Anyways.. An anonymous enum type is seemingly a special case of a trait object, so the syntax We should thus explore type erasure for trait objects before worrying about the enum special cases being discussed here. Is there any serious proposal even for generic tooling to say supersede the |
Centril
referenced this issue
Oct 9, 2018
Closed
Figure out a way to ergonomically match against impl traits? #2261
This comment has been minimized.
This comment has been minimized.
mqudsi
commented
Oct 9, 2018
|
See also the discussion in #2261, which has been closed in favor of this issue. |
glaebhoerl
referenced this issue
Nov 7, 2018
Closed
RFC for anonymous variant types, a minimal ad-hoc sum type #2587
This comment has been minimized.
This comment has been minimized.
newpavlov
commented
Nov 7, 2018
|
I've wrote a pre-RFC which includes functionality requested in this issue. |
This comment has been minimized.
This comment has been minimized.
taiki-e
commented
Jan 24, 2019
|
I wrote this feature with procedural macro: auto_enums. #[auto_enum(Iterator)]
fn foo(x: i32) -> impl Iterator<Item = i32> {
match x {
0 => 1..10,
_ => vec![5, 10].into_iter(),
}
}Several other points:
|
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Jan 24, 2019
|
@taiki-e Good stuff! This looks like a well-engineered approach. |
Ekleog commentedApr 23, 2018
•
edited
First, the idea was lain down by @glaebhoerl here.
The idea is basically to have a way to tell the compiler “please auto-generate me a sum trait for my
-> impl Trait” function, that would automatically deriveTraitbased on the implementations of the individual members.There would, I think, be a need for a syntax specially dedicated to this, so that people are not auto-generating these without being aware of it. Currently, the two syntaxes I can think of are either
|value|(without a following{}), running on the idea of “this is auto-generated, just like closures” (I don't like it much, but it could make sense), and the other idea is to make it use a keyword. I'd have gone withauto, but it appears to not be reserved, andbecomeorenumwould likely be the best choices.(Edit: @Pauan pointed out that the
|…|syntax would be ambiguous in certain cases, so please disregard it)So the syntax would, I think, look like this:
The major advantage of the
||syntax is that it doesn't raise the issue of parenthesizing, as it's already made of parenthesis.What do you think about this idea, especially now that
impl Traitis landing and the need is getting stronger?