all lazy structures are not stack safe
See original GitHub issueAll lazy statures (IO
, Future
, State
) have stack issue. general way of implementing operations on such structures is:
function Structure(f) {
if (!(this instanceof Structure)) {
return new Structure(f);
}
this.run = f
}
Structure.prototype.method = function (a){
return new Structure(function(){
// here we call `run` from a or something like that
})
}
All of lazy structures need extra stack frame on each map/ap/chain/...
, as they are creating new functions which have use old computation or passed in function, requiring larger stack size, leading to stack overflow (when you run/execute/lower them)
- apply curried function
f
which is taking 10000 arguments:
...(T.of(9999).ap(T.of(10000).ap(T.of(f)))
- map on some structure 10000 times
T.of(1).map(id).map(id).map(id).map(id).map(id).....
btw there is no such issue with native Promise:
Array(40000)
.fill(idx => a=>a + idx)
.reduce(
(p,f,idx) => p.then(f(idx)),
new Promise((res,rej) => { setTimeout(res, 1000, 1)})
).then(console.log.bind(console))// 799980001
General way to fix this would be rewrite such structures so that they builds up computation as data on map/ap/chain/...
(like Free monad/applicative…) and executing run
interpret it in a stack safe way. For example this way we can make function composition safe. Also related paper “Stack safety for free” by @paf31.
what others think on this issue?
Issue Analytics
- State:
- Created 7 years ago
- Comments:16 (14 by maintainers)
Top GitHub Comments
@SimonRichardson PTC is still in the spec, and VM writers are supposed to implement it. There’s a proposal to require a token for calls that should be a tail call, but that hasn’t left draft stage yet, and as I understand part of TC39 objects to that (in particular because you don’t really have PTC at that point) — either way, we’d get some sort of tail calls though.
Tail calls solve the same problem trampolining does, they’re just cheaper (you can translate them to a simple jump with the appropriate registers / argument stack updated), you just need to write your code such that all relevant calls would be in tail position (not the case with the Io example above —
f(this.run())
doesn’t havethis.run()
in tail position —, but Data.Task’s current implementation pretty much translates things into CPS, as does fantasy-promises in this organisation, since you need to for asynchronous code anyway).So:
Because all of these calls are in tail position, the VM doesn’t need to return a value to once it finishes evaluating the function, so it can translate it to something like:
Doing CPS translations manually to verify these is quite a pain, but you really only need to ensure that your call happens in tail position. If it does, the VM doesn’t need to translate that into a CALL instruction, it can just use a JUMP, and then you don’t have a stack problem.
The only problem with this so far is that not all VMs have implemented PTC 😦
It may be worth noting that Fluture hasn’t been affected since its 6.x release. Since that means that providing a stack safe implementation can be up to the library author, I suppose this issue could be closed.