Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I break out of recursive IEnumerable<T> loops using yield break?

I have the following method that works well, except the yield break statement only breaks out of the current enumerator. I understand why this is the case, but I am drawing a blank over how to propogate the yield break up through the recursive stack.

    private static IEnumerable<Node> FindChildrenById(IEnumerable nodes, string parentText) {
        var en = nodes.GetEnumerator();
        var targetFound = false;
        while (en.MoveNext())  {
            var node = en.Current as Node;
            if (node != null) 
            {
                if (node.Parent == null && string.IsNullOrEmpty(parentText))
                {
                    //Returns the top level nodes if an empty parentIdis entered
                    targetFound = true;
                    yield return node;
                }
                else if (node.Parent != null && node.Parent.Text == parentText)
                {
                    //returns the nodes belonging to the parent
                    yield return node;
                }
                else
                {
                    //Recurse into the children to see whether one of these is the node to find
                    foreach (var nd in FindChildrenById(node.Nodes, parentText))
                    {
                        yield return nd;
                    }
                }
            }
        }
        if (targetFound)
        {
            yield break;
        }
    }

So when I have the following nodes and pass "Top 2 a" as the parentText...

Top 1
    Top 1 a
    Top 1 b
Top 2
    Top 2 a
       Top 2 aa
       Top 2 ab
       Top 2 ac
    Top 2 b
Top 3
    Top 3 a
    Top 3 b
Top 4

... then I get the result:

Top 2 aa
Top 2 ab
Top 2 ac

This is the correct result, however, when I step through my code, the outer-most loop continues to process Top 3 and Top 4. How do I break out of this outer loop?

like image 965
Daniel Dyson Avatar asked Oct 15 '22 02:10

Daniel Dyson


2 Answers

If I got your code right, I guess the code below will solve your problem

private static IEnumerable<Node> FindChildrenById(IEnumerable nodes, string parentText)
    {
        var result =
               (from node in nodes
                where (node.Parent == null && string.IsNullOrEmpty(parentText))
                      || (node.Parent != null && node.Parent.Text == parentText)
                select node).TakeWhile(node => !(node.Parent == null && string.IsNullOrEmpty(parentText)));
        return result;
    }

It's built on two extension methods (see below) and should only iterate until your target found criteria is met

public static class IEnumerablExtensions
        {
            //Will iterate the graph in depth first order
            public static IEnumerable<TResult> Select<TResult>(this IEnumerable collection, Func<Node, TResult> selector)
            {
                foreach (var obj in collection)
                {
                    var node = obj as Node;
                    if (node != null)
                    {
                        yield return selector(node);
                        foreach (var n in node.Nodes.Select(selector))
                        {
                           yield return n;
                        }
                    }
                }
            }

        public static IEnumerable<Node> Where(this IEnumerable collection, Predicate<Node> pred)
        {
            foreach (var node in collection.Select(x => x)) //iterate the list in graph first order
            {
                if (pred(node))
                    yield return node;
            }
        }
    }

EDIT: There was an error in the Select method in the original posting (it didn't iterate the children of children) that is now corrected

like image 89
Rune FS Avatar answered Oct 20 '22 17:10

Rune FS


I'm assuming that the function is actually named FindChildrenById, otherwise I can't see any recursion going on.

Under this assumption you can either use exceptions (which I would strongly recommend against), or return a KeyValuePair<bool, IEnumerable<Node>> where the bool part will be used to signal an early out up the chain.

Then, on the API level, expose a wrapper method that simply returns the IEnumerable<Node> part and throws way the bool part.

Here's an example, given the class Node:

public class Node
{
    List<Node> children;
    public string Text { get; set; }
    public List<Node> Children { get { return children ?? (children = new List<Node>()); } }
}

You can traverse and shortcut like this:

public class NodeTraverser
{
    private static KeyValuePair<bool, IEnumerable<Node>> GetChildrenById(string text, Node node)
    {
        if(node.Text == text)
        {
            return new KeyValuePair<bool,IEnumerable<Node>>(true, node.Children);
        }
        foreach(var child in node.Children)
        {
            var result = GetChildrenById(text, child);
            if(result.Key)
            {
                return result; // early out
            }
        }
        return new KeyValuePair<bool,IEnumerable<Node>>(false, Enumerable.Empty<Node>());
    }

    public static IEnumerable<Node> FindChildrenbyId(string text, Node root)
    {
        return GetChildrenById(text, root).Value; 
    }
}
like image 25
corvuscorax Avatar answered Oct 20 '22 16:10

corvuscorax