Reification call stack by coroutine
From the article Philosophy of coroutines, the author mentions a 'stunf' of coroutines:
Another slightly unexpected ‘stunt’ use of coroutines is to use them to reconstruct the call-stack structure of ordinary subroutines: replace every normal function call with a coroutine-style yield operation which makes a newly constructed instance of the subroutine start running next, and whenever a coroutine terminates, resume the one that invoked it.
This reminds me Daniel Friedman's The Role of the Study of Programming Languages in the Education of a Programmer". The code below is also a correctness-preserved transformation.
Here is my try in Lua, in which coroutine is first-class value.
We want to transfer some recursive function to the form with explicit call stack structure. Here is an example of counting element of array in recursive style:
local function length_rec(t)
if t == nil or #t == 0 then
return 0
end
return 1 + length_rec({table.unpack(t, 2)})
end
And re-write it with coroutine will be:
local function length(t)
if t == nil or #t == 0 then
coroutine.yield("ret", 0)
else
local l = coroutine.yield("call", {table.unpack(t, 2)})
coroutine.yield("ret", l + 1)
end
end
And we need to create infrastructure to run this coroutine style function. Here is one possible implementation:
local function run(f, arg)
local init_co = coroutine.create(f)
local stacks = {{init_co, "init", arg}}
while #stacks ~= 0 do
local frame = stacks[#stacks]
local co = frame[1]
local frame_type = frame[2]
local frame_val = frame[3]
if frame_type == "ret" then
table.remove(stacks)
if #stacks == 0 then
return frame_val
else
co = stacks[#stacks][1]
end
elseif frame_type == "call" then
co = coroutine.create(f)
-- else frame_type == "init" then
-- just use the current frame.
end
local _, new_frame_type, new_frame_val = coroutine.resume(co, frame_val)
if frame_type == "ret" or frame_type == "init" then
stacks[#stacks][2] = new_frame_type
stacks[#stacks][3] = new_frame_val
else
table.insert(stacks, {co, new_frame_type, new_frame_val})
end
end
end
And run it:
print(run(length, {1, 2, 3, 4}))
-- print 4
Then here is another a little bit more complex example:
local function tree_sum(t)
if t == nil then
coroutine.yield("ret", 0)
else
local l = coroutine.yield("call", t['left'])
local r = coroutine.yield("call", t['right'])
coroutine.yield("ret", l + r + t['v'])
end
end
Run it:
print(executor(tree_sum, {v=1, left={v=2, left={v=4}}, right={v=3, right={v=5}}}))
-- print 15
Application of the tranformation
The following case will run into "stack overflow" error.
local function sum_rec(n)
if n == 0 then
return 0
else
local s = sum_rec(n - 1)
return s + n
end
end
print(sum_rec(1000000))
But with this transformation and re-written, it can produce result correctly:
local function sum(n)
if n == 0 then
coroutine.yield("ret", 0)
else
local s = coroutine.yield("call", n - 1)
coroutine.yield("ret", s + n)
end
end
print(executor(sum, 1000000))
-- print 500000500000