1

I'm trying to implement basic lazy sequences in JavaScript. I'm only using closures and continuations. This is what I got so far:

var cons = curry(function(x, y, list){
  return list(x, y);
});
var head = function(seq){
  return seq(function(x, y){
    return x;
  });
};
var tail = function(seq){
  return seq(function(x, y){
    return y();
  });
};
var iterate = function(f){
  var next = f();
  if (next != null) {
    return cons(next, function(){
      return iterate(f);
    });
  }
};
var take = curry(function(n, seq){
  if (n && seq != null) {
    return cons(head(seq), function(){
      return take(n - 1, tail(seq));
    });
  }
});
var doSeq = curry(function(n, f, seq){
  while (n-- && seq != null) {
    f(head(seq));
    seq = tail(seq);
  }
});

var rand = iterate(Math.random);
var log = function(x){ console.log(x) };

doSeq(10, log, rand); //=> logs 10 random numbers

I didn't post the curry function as it isn't directly related to the question.

Now the deal breaker is filter. The canonical implementation is tail recursive:

var filter = curry(function(f, seq){
  if (seq == null) {
    return;
  }
  if (!f(head(seq))) {
    return filter(f, tail(seq)); // recursion
  }
  return cons(head(seq), function(){
    return filter(f, tail(seq));
  });
});

When I run it many times on a sequence the stack will eventually blow up:

Imgur

I know a common workaround is to use a trampoline, and that's relatively easy in an eager world, but it seems daunting to implement it for a lazy sequence. I found an intricate solution in Scheme, but I gave up on trying to implement it as-is in JavaScript.

Is this as complicated as it seems? Is there another way to solve this issue, with iteration maybe? Any hints on porting the Scheme code to JavaScript in a sane way?

elclanrs
  • 92,861
  • 21
  • 134
  • 171
  • Btw, your `iterate` function is not very functional, as it does not save any state. Use `function iterate(f, cur){ if (cur != null) { var next = f(cur); return cons(cur, function(){ return iterate(f, next); }); } }` so that `var ints = iterate(function(x){return x+1}, 0)` – Bergi Mar 05 '14 at 17:30
  • Yeah, will try that, I'm simply using an IIFE closure to keep track of the value for now as you see in the JSBin demo. – elclanrs Mar 05 '14 at 17:35
  • Yes, but that doesn't work. `head(tail(ints)) != head(tail(ints))` - wait, what?! Every `tail` call increases the result by one. – Bergi Mar 05 '14 at 17:39
  • I'm not following... It works fine, I'm not sure what you mean, I tried ints, fibos... http://jsbin.com/taqij/1/edit – elclanrs Mar 05 '14 at 17:43
  • Try http://jsbin.com/kugajala/2/edit - I would've expected to get `1` always when alerting the first `int` – Bergi Mar 05 '14 at 17:50
  • Oh I see, that looks indeed like an issue. – elclanrs Mar 05 '14 at 17:55
  • Not knowing enough JS, but the problem seems to be that cons() does not work tail-lazy. What this means is that cons must never evaluate its 2nd argument (the tail of the list that gets constructed). This should be solely the job of tail() – Ingo Mar 05 '14 at 18:22
  • @Bergi: I ended up just doing this http://jsbin.com/kugajala/6/edit. – elclanrs Mar 05 '14 at 18:28
  • @elclanrs: no no no! What about `var some = take(10, fibo()); tail(some)(alert); tail(some)(alert)`? You're effectively destroying any sharing. Believe me, this will bite you in more complex examples. – Bergi Mar 05 '14 at 18:33
  • @Bergi: Aight, point taken, I would've discovered this myself and wasted a bunch of time, thanks for the input. – elclanrs Mar 05 '14 at 18:38

1 Answers1

2

I think this should do it while still being lazy1:

var filter = curry(function(f, seq){
  while (seq != null && !f(head(seq)))
      seq = tail(seq);
  if (seq == null)
    return;
  return cons(head(seq), function() {
    return filter(f, tail(seq))
  });
});

Maybe easier readable:

var filter = curry(function(f, seq){
  for (; seq != null; seq = tail(seq))
    if ( f(head(seq)) )
      return cons(head(seq), function() {
        return filter(f, tail(seq))
      });
});

1): Your representation of lazy sequences does not seem to have a way to express a maybe-empty (yet undetermined) list

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • If this is still as lazy as the recursive version, is the Scheme solution overkill or even necessary? Btw, I already experimented with `constant`, but it has a problem, it will blow up the stack depending on the order of composition, see http://jsbin.com/jevom/1/edit and http://jsbin.com/qamiq/1/edit – elclanrs Mar 05 '14 at 17:13
  • Ouch, of course that `constant` function is *not* lazy evaluated - I'm too much in Haskell :-/ – Bergi Mar 05 '14 at 17:27
  • Ok, so I think I'm going with this. I was too concerned with trampolining a recursive function that I missed the obvious... Thanks. – elclanrs Mar 05 '14 at 17:38
  • Actually I think `seq = tail(seq)` in a loop *is* trampolining :-) – Bergi Mar 05 '14 at 17:40
  • Yes, in general I suppose it is, but the Scheme solution seems to suggest a way to trampoline a promise that will execute the thunk later, even more lazily, if I understand correctly. – elclanrs Mar 05 '14 at 17:45