parliament
parliament

Reputation: 22924

Typescript function using recursion and yield keyword to pull out nested lists

I'm trying to rewrite a C# function that uses yield and recursion to pull out instances of a class out of nested lists into a single list.

This is the C# function:

 public static IEnumerable<TargetObject> GetRecursively(params TargetObject[] startingObjects)
 {
    foreach (TargetObject startingObject in startingObjects)
    {
        yield return startingObject;
        if (startingObject.InnerObjects != null)
            foreach (TargetObject innerObject in startingObject.InnerObjects.ToArray())
                foreach (TargetObject recursiveInner in GetRecursively(innerObject))
                    yield return recursiveInner;
     }
 }

Being that javascript doesn't reliably support yield across browsers, how can I simulate it in this complex function?

function getRecursively(...startingObjects: TargetObject[])
{
    return function () {
           ??
    }
}

Upvotes: 4

Views: 6847

Answers (3)

JasonS
JasonS

Reputation: 7733

Update for 2019

This is now super easy with typescript (or es6)

function* recursiveIterator( obj: { children?: any[]; } ):IterableIterator<any> {
    yield obj;
    for ( const child of obj.children ) {
        yield* recursiveIterator( child );
    }
}

Upvotes: 9

Fenton
Fenton

Reputation: 250992

If you want to run some code for each of the items, you can pass in the callback to be executed. This allows similar behaviour to the iterator pattern as the looping will pause while the callback is executed, and then continue once the callback finishes.

This is useful if the iteration itself is obtaining data dynamically that you don't want to hold in memory for the whole process or if you want to avoid flattening the array.

This isn't the same as using yield in C# but it is just as simple - all you need to do is write a function that will run the code against each item found.

Here is an example:

class TargetObject {
    constructor(public name: string, public innerObjects: TargetObject[]) {
    }

    static forEachRecursive(callback: (item: TargetObject) => any, targetObjects: TargetObject[]){
        for (var i = 0; i < targetObjects.length; i++) {
            var item = targetObjects[i];

            callback(item);

            if (item.innerObjects) {
                TargetObject.forEachRecursive(callback, item.innerObjects);
            }
        }
    }
}

var targetObjects = [
    new TargetObject('Example', []),
    new TargetObject('With Inner', [
        new TargetObject('Inner 1', []),
        new TargetObject('Inner 2', [])
    ])
];

var callback = function (item: TargetObject) {
    console.log(item.name);
};

TargetObject.forEachRecursive(callback, targetObjects);

Upvotes: 1

basarat
basarat

Reputation: 276027

The way the yield keyword works internally is by creating a state machine. You can create one yourself, or if the list is not too large and you can reasonably keep it in memory you can simply return and use a list e.g:

function getRecursively(...startingObjects:TargetObject[] ):TargetObject[] 
 {
    var toreturn = [];
    for (var key in startingObjects)
    {
        var startingObject = startingObjects[key];
        toreturn.push(startingObject);
        if (startingObject.InnerObjects != null)
            for (var key2 in startingObject.InnerObjects){
                var innerObject = startingObject.InnerObjects[key2];
                var superInner = getRecursively(innerObject);
                for (var key3 in superInner)
                    toreturn.push(superInner[key3]);                        
            }
     }
    return toreturn; 
 }

If you really really want yield you could use the google traceur compiler : https://github.com/google/traceur-compiler e.g.

function* cool() {
  yield 123;    
  yield 456;  
}

for (n of cool()) {    
    console.log(n);
}  

Try it online

As you can see the generated state machine is not trivial.

var $__generatorWrap = function(generator) {
  return $traceurRuntime.addIterator({
    next: function(x) {
      switch (generator.GState) {
        case 1:
          throw new Error('"next" on executing generator');
        case 3:
          throw new Error('"next" on closed generator');
        case 0:
          if (x !== undefined) {
            throw new TypeError('Sent value to newborn generator');
          }
        case 2:
          generator.GState = 1;
          if (generator.moveNext(x, 0)) {
            generator.GState = 2;
            return {
              value: generator.current,
              done: false
            };
          }
          generator.GState = 3;
          return {
            value: generator.yieldReturn,
            done: true
          };
      }
    },
    'throw': function(x) {
      switch (generator.GState) {
        case 1:
          throw new Error('"throw" on executing generator');
        case 3:
          throw new Error('"throw" on closed generator');
        case 0:
          generator.GState = 3;
          throw x;
        case 2:
          generator.GState = 1;
          if (generator.moveNext(x, 1)) {
            generator.GState = 2;
            return {
              value: generator.current,
              done: false
            };
          }
          generator.GState = 3;
          return {
            value: generator.yieldReturn,
            done: true
          };
      }
    }
  });
};
function cool() {
  var $that = this;
  var $arguments = arguments;
  var $state = 0;
  var $storedException;
  var $finallyFallThrough;
  var $G = {
    GState: 0,
    current: undefined,
    yieldReturn: undefined,
    innerFunction: function($yieldSent, $yieldAction) {
      while (true) switch ($state) {
        case 0:
          this.current = 123;
          $state = 1;
          return true;
        case 1:
          if ($yieldAction == 1) {
            $yieldAction = 0;
            throw $yieldSent;
          }
          $state = 3;
          break;
        case 3:
          this.current = 456;
          $state = 5;
          return true;
        case 5:
          if ($yieldAction == 1) {
            $yieldAction = 0;
            throw $yieldSent;
          }
          $state = 7;
          break;
        case 7:
          $state = -2;
        case -2:
          return false;
        case -3:
          throw $storedException;
        default:
          throw "traceur compiler bug: invalid state in state machine: " + $state;
      }
    },
    moveNext: function($yieldSent, $yieldAction) {
      while (true) try {
        return this.innerFunction($yieldSent, $yieldAction);
      } catch ($caughtException) {
        $storedException = $caughtException;
        switch ($state) {
          default:
            this.GState = 3;
            $state = -2;
            throw $storedException;
        }
      }
    }
  };
  return $__generatorWrap($G);
}
for (var $__0 = $traceurRuntime.getIterator(cool()), $__1; !($__1 = $__0.next()).done;) {
  n = $__1.value;
  {
    console.log(n);
  }
}

Upvotes: 4

Related Questions