Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reuse coroutine with different arguments per call in lua

I found it really useful to reuse a once created coroutine. I found a solution to that and it looks like so:

co = coroutine.create(function (f, args)
  while f do
    f = coroutine.yield(f(args))
  end
end)

function dummyFunc(data)
  print("XXX "..data)
  coroutine.yield()
  print("OOO "..data)
end

coroutine.resume(co, dummyFunc, "1")
coroutine.resume(co, dummyFunc, "2")
coroutine.resume(co, dummyFunc, "3")
coroutine.resume(co, dummyFunc, "4")

That work like a charm except the output is not:

XXX 1
OOO 2
XXX 3
OOO 4

It is:

XXX 1
OOO 1
XXX 1
OOO 1

So is it possible to change the arguments to the dummyFunc between the resume calls?

like image 895
milkpirate Avatar asked Oct 20 '22 02:10

milkpirate


1 Answers

Think about this. The way coroutines work is like this. When you first resume them, the arguments you pass to resume become the arguments to the coroutine's function. When the coroutine yields, the arguments it passes to yield become the return values from your resume call.

However, the second time you resume the coroutine, it does not reach into the still executing function and change the arguments that were pass in the first time. It would be exceedinly rude to change the value of variables local to the function.

Therefore, the arguments to resume on calls after the first call will be the return values from yield.

co = coroutine.create(function (f, args)
  while f do
    f = coroutine.yield(f(args))
  end
end)

So you'd need to do this:

co = coroutine.create(function (f, args)
  while f do
    f, args = coroutine.yield(f(args))
  end
end)

However, if you want something more flexible, that can do variable numbers of arguments, you'll need to be cleverer:

co = coroutine.create(function (...)
  local function capture_args(...)
    return {...}, select("#", ...)
  end

  local tbl, len = capture_args(...)
  local f = tbl[1]

  while f do
    tbl, len = capture_args(coroutine.yield(f(unpack(tbl, 2, len))
    f = tbl[1]
  end
end)

Some people wouldn't bother with the capture_args stuff, simply relying on {...} and calling unpack on it. This is safer because users can put nil values in parameter lists. ... will record all of the parameters, even embedded nils (but not trailing ones). However, once you put it into an array, the length of the array is based on the first nil value.

Using capture_args, you can get the actual parameter count thanks to a little-known-feature of select. And thanks to the ability of unpack to work on a given range, even if the range exceeds the length of the table, you can effectively store a parameter list.

I could probably make capture_args a bit cleverer by putting the length in the table it returns. But this is good enough for me.


There is a second problem here: you are yielding within dummyFunc, and dummyFunc does not seem to understand what to do with yield's return values (ie: the parameters to your next resume call).

It's not clear how you want dummyFunc to respond to it. If you wanted dummyFunc's parameters to change because of how you resumed it without dummyFunc knowing about it, that's not going to happen.

like image 99
Nicol Bolas Avatar answered Oct 23 '22 06:10

Nicol Bolas