Replies: 40 comments 218 replies
-
This sure would be nice. |
Beta Was this translation helpful? Give feedback.
-
This feature would of course be really nice, but when it was originally proposed on CodePlex Lucian specifically addressed Erik Meijer's paper and mentioned that implementing this would make the common case more slow. I'm guessing that's a tradeoff the team is unlikely to make. |
Beta Was this translation helpful? Give feedback.
-
I would like to add two use-cases to the discussion. Most discussions of recursive iterators simply assume that
A simple binary tree: class Tree {
Tree L; // left subtree (node values smaller than V)
int V; // node value
Tree R; // right subtree (node values larger than V)
} can be enumerated by this recursive iterator: IEnumerable<int> Iterator1 (Tree t) {
if (null == t) return;
foreach (var vl in Iterator1 (t.L)) yield return vl;
yield return t.V;
foreach (var vr in Iterator1 (t.R)) yield return vr;
} In a future version of C#, this might be shortened to: IEnumerable<int> Iterator2 (Tree t) {
if (null == t) return;
yield foreach Iterator2 (t.L);
yield return t.V;
yield foreach Iterator2 (t.R);
} But what if some processing of all items is required ? 1) Iterators with item processing For example, this iterator converts each int item to string and appends IEnumerable<string> Iterator3 (Tree t) {
if (null == t) return;
foreach (var vl in Iterator3 (t.L)) yield return $"{vl}L";
yield return t.V;
foreach (var vr in Iterator3 (t.R)) yield return $"{vr}R";
} which converts the balanced tree: The new, shorter syntax needs some way to specify an optional operation for each item, IEnumerable<string> Iterator4 (Tree t) {
if (null == t) return;
yield foreach Iterator4 (t.L) vl => $"{vl}L";
yield return t.V;
yield foreach Iterator4 (t.R) vr => $"{vr}R";
} If the lambda function is omitted, 2) Mutual recursive iterators Lets add another, completly artificial requirement: IEnumerable<string> Iterator5a (Tree t) { // ascending (L,V,R)
if (null == t) return;
foreach (var vl in Iterator5d (t.L)) yield return $"{vl}L";
yield return t.V;
foreach (var vr in Iterator5d (t.R)) yield return $"{vr}R";
}
IEnumerable<string> Iterator5d (Tree t) { // descending (R,V,L)
if (null == t) return;
foreach (var vr in Iterator5a (t.R)) yield return $"{vr}R";
yield return t.V;
foreach (var vl in Iterator5a (t.L)) yield return $"{vl}L";
} or shorter: IEnumerable<string> Iterator6a (Tree t) { // ascending (L,V,R)
if (null == t) return;
yield foreach Iterator6d (t.L)) vl => $"{vl}L";
yield return t.V;
yield foreach Iterator6d (t.R)) vr => $"{vr}R";
}
IEnumerable<string> Iterator6d (Tree t) { // descending (R,V,L)
if (null == t) return;
yield foreach Iterator6a (t.R)) vr => $"{vr}R";
yield return t.V;
yield foreach Iterator6a (t.L)) vl => $"{vl}L";
} Yes, the example is idiotic. Instead assume traversing some arbitrary object graph So my questions are:
|
Beta Was this translation helpful? Give feedback.
-
I think the solution in Eric Lippert's blog post is still simple enough (10 extra lines of code and can easily be generalized for any iterator) is simple enough that it really isn't necessary. It would be nice, but we can already get the optimization today. |
Beta Was this translation helpful? Give feedback.
-
@MillKaDe I think adding item processing does not make any sense. Your code has a quadratic number of string formatting operations and I don't see how could the compiler fix that. And, if I understand it correctly, the proposed implementation would not support this. When it comes to mutual recursion (without item processing), I think that should work just fine. |
Beta Was this translation helpful? Give feedback.
-
Keywords for anyone else who might come along looking for this: |
Beta Was this translation helpful? Give feedback.
-
IEnumerable<string> Iterator4 (Tree t) {
if (null == t) return;
yield foreach Iterator4 (t.L) vl => $"{vl}L";
yield return t.V;
yield foreach Iterator4 (t.R) vr => $"{vr}R";
} This could also be accomplished with existing language features + IEnumerable<string> Iterator4 (Tree t) {
if (null == t) return;
yield foreach (from v in Iterator4(t.L) select $"{v}L");
yield return t.V;
yield foreach (from v in Iterator4(t.R) select $"{v}R");
} Or, even better: IEnumerable<string> Iterator4 (Tree t) {
if (null == t) return;
yield foreach Iterator4(t.L).Select(v => $"{v}L");
yield return t.V;
yield foreach Iterator4(t.R).Select(v => $"{v}R");
} If anything, an extension to LINQ query syntax might make more sense than lambdas floating around, something like IEnumerable<string> Iterator4 (Tree t) {
if (null == t) return;
from v in Iterator4(t.L) yield $"{v}L";
yield return t.V;
from v in Iterator4(t.R) yield $"{v}R";
} But I'm not sure even that is really necessary... the existing LINQ tools are more than adequate for manipulating |
Beta Was this translation helpful? Give feedback.
-
👍 for this feature! |
Beta Was this translation helpful? Give feedback.
-
I created next suggestion for |
Beta Was this translation helpful? Give feedback.
-
We have syntax for |
Beta Was this translation helpful? Give feedback.
-
I would love to have this! |
Beta Was this translation helpful? Give feedback.
-
Would definitely like this functionality. And I can't imagine it's too difficult to design and implement. |
Beta Was this translation helpful? Give feedback.
-
There's been no actual discussion of this feature for > 3 years. Just ~20 distinct requests for it. Is it still being considered as a useful feature by the languages development team? |
Beta Was this translation helpful? Give feedback.
-
@Brondahl there is no one currently championing this. |
Beta Was this translation helpful? Give feedback.
-
Adding a link to this article: https://docs.microsoft.com/en-us/archive/blogs/wesdyer/all-about-iterators because I don't think it has been mentioned yet in this issue, and it has a good description of how a "yield foreach" might result in performance optimizations for some cases, which I think is what has been discussed in the above comments somewhat.
The article was written by Wes Dyer of Microsoft in 2007. |
Beta Was this translation helpful? Give feedback.
-
Correct me if I'm wrong but isn't the non-quadratic part of this request an implementation detail? That should be left for Roslyn to figure out. Such a request could then be generalized to all recursive iterators and not necessarily just for this new syntax. |
Beta Was this translation helpful? Give feedback.
-
@Thaina , @austinw-fineart ... @CyrusNajmabadi has been very clear that he understands the request, and that he understands that the sugar syntax doesn't change the performance. But that in his view the risk of adding a syntax that could give a misleading impression that they have solved the perf problem, out weighs any possible advantage of the one-line syntactic sugar.
|
Beta Was this translation helpful? Give feedback.
-
JS dev: "What's up C# guys! What's the yield* equivalent? F# dev: "What's up C# guys! What's the yield! equivalent? Seriously, it's just embarrassing. |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi I want to circle this conversation back to the main topic/blocker, being the performance issue. I have a couple questions, just so I have a better idea of the likelihood of this feature being added in the near-ish future.
|
Beta Was this translation helpful? Give feedback.
-
I've now raised #5503 which addresses the sugar-only ... non-quadratic aspect of the discussion 👍 I imagine that some of the people that voiced opinions on this thread, re the syntactic sugar may wish to summarise / reproduce them over there. |
Beta Was this translation helpful? Give feedback.
-
As a possible dumb solution, why not add a hint via an attribute or contextual keyword to do the special casing for recursive iterators to enable the syntax? public recursive IEnumerable<T> TreeWalk<T>(BinaryNode<T> root)
{
if(root is null) yield break;
yield return foreach TreeWalk(root.Left);
yield return root.Value;
yield return foreach TreeWalk(root.Right);
} Without the recursive keyword, the compiler will generate the equivalent of If a new contextual keyword is unpalatable (as previous discussions have suggested), could it maybe that you can write the method with a different return type? |
Beta Was this translation helpful? Give feedback.
-
could we have :
The compiler can compile the foreach for IRecursiveEnumerable differently to just a recursive enumerable. unwraping a recursiveEnumerable to an enumerable by enumerating it is fine, as you have already unfolded the quadratic. It feels like we could setup something where a recursiveEnumerable is convertable to a normal enumerable, and that cast contains the machinery for passing the existing enumerator down through the chain, absorbing the quadratic cost. downsides
Thoughts? |
Beta Was this translation helpful? Give feedback.
-
I created the The library has benchmarks to show that quadratic complexity was decreased to linear complexity. Also, those benchmarks are examples of code refactoring. I am planning to create a source generator with such automatic code refactoring. If you have any suggestions or comments on the library, please write about it. I will also be appreciated in any help to create the documentation in good English. |
Beta Was this translation helpful? Give feedback.
-
Is this on a roadmap ? recursive yields are very common when processing tree structures such (e.g. Json) Both JS and F# seem to have a good solution. |
Beta Was this translation helpful? Give feedback.
-
Would this proposal capture Similar to what's being discussed here: https://stackoverflow.com/questions/78438765/multi-yield-mechanism-in-c-sharp?noredirect=1#comment138285851_78438765 |
Beta Was this translation helpful? Give feedback.
-
With yield return ..list; |
Beta Was this translation helpful? Give feedback.
-
I don't understand why this has not been adopted. All that's needed to operate a yield each is an execution stack, which already exists to operate existing yield operators. When yield each is compiled, it bypasses containering of IEnumerable (i.e. creating new IEnumerable and associated MoveNext, etc. methods), directly calling underlying iterator code just like calling any other function, momentarily breaking at yield return or going up call stack at end of function or yield break. If yield each is being used for non-iterator, like array, it would compile to for loop to create iterator locally. This optimizes execution while also keeping the code easy to read/understand. As an example, looking at op's BinaryNode, it's much easier to read and understand the result of using yield each than to read the optimized version that directly uses a stack to get the same result, using yield return (it would be even worse if yield operator didn't exist). Easy to read, but is very inefficient
Easy to read and can be optimized by compiler
Hard to read, but is optimized
|
Beta Was this translation helpful? Give feedback.
-
10 years later, I still don't understand the problem. This is just syntactical sugar! A no-brainer. Implement it already. If the compiler can handle this: foreach (var item in items)
{
yield return item;
} Sure it can also handle a shortened version: yield each items; Same |
Beta Was this translation helpful? Give feedback.
-
Here is a gist of a document I presented to C# LDM on this issue a few years ago. The final source code created during the exploration is here: |
Beta Was this translation helpful? Give feedback.
-
Can someone shed some light on how this is implemented in F# and JS? Or do they suffer from the same issue and it's just syntax sugar? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield* These are incredibly useful in practice. |
Beta Was this translation helpful? Give feedback.
-
@AdamSpeight2008 commented on Fri Jan 16 2015
Current if you want write a recursive based iterator function.
it ends up being Quadratic runtime
If I could express the Iterator / Yielder as a parameter I could linearise the runtime.
@ufcpp commented on Fri Jan 16 2015
👍
I'd like this fearure to be added to C# too.
@mattwar commented on Fri Jan 16 2015
It's a nice feature, and we had it on the list when we did IEnumerable, or the release after, not entirely sure. But it is kind of niche feature. I think the syntax was going to be something like:
Erik Meijer had an algorithm to make it near non-recursive.
@AdamSpeight2008 commented on Sat Jan 17 2015
@mattwar
Don't confuse
yield foreach recFN
withyield x On iter
yield foreach recFN
is equivalent toWhich is still Quadratic.
This does something different. Let's use another example. Yielding the permutations of a list of items.
Let's create the public method the initiates the iterator and calls the method.
Iterator.Create( _Perm<T> );
create the single instance of the iterator / yielder that is use through out the recursive calls.Maybe an attribute
[Iterator(Recusive:= True)]
on the function, creates the "linearised" state-machine.@mattwar commented on Sat Jan 17 2015
The yield foreach I was referring to was just a syntax. It did not translate to a foreach instruction and required a very different codegen for the iterator methods.
@theoy commented on Tue Jan 20 2015
I think it already has the VB tag, right?
Am I confused about something?
Cheers,
--Theo
From: Adam Speight [mailto:[email protected]]
Sent: Tuesday, January 20, 2015 3:21 PM
To: dotnet/roslyn
Cc: Theo Yaung
Subject: Re: [roslyn] Feature Request: Recursive Iterators (non-quadratic) (#15)
@theoyhttps://github.com/theoy Co-Evolution? Why no VB.net tag?
—
Reply to this email directly or view it on GitHubhttps://github.com/dotnet/roslyn/issues/15#issuecomment-70755229.
@AdamSpeight2008 commented on Tue Jan 20 2015
I missed the tag, then replied. Then saw the tag, so removed to comment. DOH!
@HaloFour commented on Tue Jan 20 2015
Didn't we discuss this in some length on the codeplex forums? If I recall while there was an alternate algorithm which made these scenarios much faster they were also found to be much slower for the common non-nested cases.
@weltkante commented on Fri May 01 2015
For reference, the paper about "yield foreach" with non-quadratic runtime (part of the Spec# work)
http://research.microsoft.com/en-us/projects/specsharp/iterators.pdf
@AlexRadch commented on Tue Jan 12 2016
I think it may be more comfortable to add
yield foreach
ofIEnumerable
as syntax sugar for loop withyield return
.@AlexRadch commented on Tue Jan 12 2016
Here dotnet/roslyn#7630 suggested yield foreach
@AdamSpeight2008 commented on Tue Jan 12 2016
@AlexRadch
My opinion
yield foreach xs
wouldn't be a good addition to the language, makes it too easy to implement the quadratic case. What do we gain by having it? 3 lines of into 1.Whereas linearisation of the recursive iterator, would be a lot more beneficial. It would be more efficient in both runtime and memory. As it wouldn't create an additional iterator for each level of recursion.
@AlexRadch commented on Tue Jan 12 2016
What is
linearisation of the recursive iterator
? What code compiler should create? Store all local variables in arrays or where? Or yourlinearisation of the recursive iterator
should create result array in memory?Now each yield fuction create object where local variables stored but where should be stored local variables in
linearised iterator
? I think if you can createlinearised code
for your iterator you can createlinearised code
foryield foreach
also.I do not see difference in code
yield foreach TreeWalk(curr.Left)
and codeTreeWalk(n.Left, Iterator)
.Can you describe difference?
linearisation of the recursive iterator
means not more thanperpetuum mobile
.@AlexRadch commented on Tue Jan 12 2016
I think you mean that
linearisation of the recursive iterator
should createList<T>
to store result and can be coded asAm I right?
@AdamSpeight2008 commented on Tue Jan 12 2016
No. Suggested reading material ( http://www.dreamincode.net/forums/topic/332455-iterators/ ).
You should now see that each recursive call (use the foreach yield) is generates a new instance of a ienumerable and ienumerator.
It should be possible generate a single instance of ienumerable and ienumerator, that encompasses all of the subsequent recursive method calls.
The compiler should capable of building a state machine for a recursive iterator function. It could internally keep a stack of method calls, where it is in each of those methods.
Non tail-recursive would potentially result in stack overflow when evaluated. Tail-recursive (along with tail call optimisation) could potentially never return. (infinite iteration).
@AlexRadch commented on Tue Jan 12 2016
It should be possible generate a single instance of ienumerable and ienumerator, that encompasses **all** of the subsequent recursive method calls.
is wrong!If it is possible to do automatic with compiler for your magic iterator, it means that it is possible do the same for
yield foreach
also. Soyield foreach
can be compiled to the same magic singleIEnumarable
with smart tail-recursion also without creation many IEnumerable!@weltkante commented on Wed Jan 13 2016
You may want to read the Microsoft research paper I linked above (which was also mentioned by mattwar) before doing handwaving and talking of magic IEnumerables. You are making too many assumptions without knowing the already presented facts.
Non-quadratic iteration (and implementation of
yield foreach
compiler magic) is possible without building a list beforehand, but for an efficient implementation it needs a new interface or some other way to extend the current IEnumerable iteration protocol.Also keep in mind that the paper only presents one possible solution, I know of at least one other possible implementation which is also non-quadratic, but it also needs a different iteration protocol.
Also note that even if new interfaces are introduced they can be optional and the iteration protocol can gracefully fall back to the classic iteration for enumerators which don't implement the new interface (at the cost of the quadratic iteration we currently have).
@AdamSpeight2008 commented on Wed Jan 13 2016
I've read the paper again, it doesn't require a new interface.
It still utilises
IEnumerable<T>
andIEnumerator<T>
Done a VB implementation
@weltkante commented on Wed Jan 13 2016
@AdamSpeight2008 then you didn't understand the code you wrote, nor the paper
That's testing for the new iterator protocol. If its not present then it falls back to quadratic behavior using a simple foreach-loop.
Using a class instead of an interface to test for a feature is just an implementation detail. It's been a year or so since I read the paper so I didn't remember their proposed implementation exactly, but the concept is the same.
If you want your custom enumerables support the new protocol they need to implement an NestedEnumerable<T> instead of an IEnumerable<T>. When using the
yield
language feature the compiler can do it for you, but existing code needs to be recompiled and explicit implementations of IEnumerable must be manually updated.@AlexRadch commented on Sat Jan 16 2016
I made hyperloop https://github.com/AlexRadch/YieldForEachDN/blob/master/Src/YieldForEachApp/Hyperloop.cs to make recursive yielded loops without quadratic runtime performance.
For example next yielded method have quadratic runtime performance because it have recursion.
To make it with liner runtime performance you should rewrite it with Hyperloop usage:
AddLoop()
used to replaceyield foreach
calls in middle recursion for more performance.AddTail()
used to replaceyield foreach
calls in tail recursion for little more performance thanAddLoop()
.@paulomorgado commented on Sat Jan 16 2016
I wonder if local function definitions could be used by the compiler to generate that linear state machine.
@weltkante commented on Sat Jan 16 2016
@paulomorgado As far as I understood it, local functions add no benefit for the compiler because its only a language feature and not an IL feature, the compiler could already generate the IL for local functions if he needed it.
Anyways, the whole reason why you need a state machine in the first place is because the control flow is non-local (you return to the caller between states) so local function definitions are unlikely to help.
@AlexRadch commented on Sat Jan 16 2016
To reduce quadratic performance in recursive yielded methods you should create fist loop (I called it hyperloop) and send to that hyper loop workitems without creating local loop in local loop in local loop and so on for each recursive call.
Here hyperloop code https://github.com/AlexRadch/YieldForEachDN/blob/master/Src/YieldForEachApp/Hyperloop.cs
Hyperloop execute recursive work in one loop without layered loops for each recursive call and then return control to continue execution if AddLoop() was used. For tail recursion you can use AddTail() so it does not return control.
Next code create Hyperloop and add first loop to them
Next code is like usual yielded method with recursion but it does not create layered loops for each recursive call but add loops to Hyperloop and Hyperloop execute work without layers (than speedup performance from quadratic to linear)
@AdamSpeight2008 commented on Sat Jan 16 2016
@weltkante @paulomorgado VB has iterator / async lambda functions, they won't help in this situation. As any state information held by them is local to that lambda.
`
@paulomorgado commented on Mon Jan 18 2016
@weltkante, local functions can be reentrant and do not need a delegate invocation. There's a lot that can be done and done better with local functions then with delegates.
Of course the compiler can generate the IL. It already does.
@gafter commented on Mon Mar 20 2017
We are now taking language feature discussion on https://github.com/dotnet/csharplang for C# specific issues, https://github.com/dotnet/vblang for VB-specific features, and https://github.com/dotnet/csharplang for features that affect both languages.
Beta Was this translation helpful? Give feedback.
All reactions