There’s one disadvantage of the described pattern - the state is opaque. This will become problematic in some debugging situations - the function is basically opaque and you can’t “look into” it with default logging to figure out what’s going on (unless you use some tricks like :erlang.fun_info(fun, :env)
to get the data bound in the closure).
I’d propose a slightly different, but I believe similarly convenient mechanism of a “continuation token”:
def next_job(token) do
case Batcher.next(token) do # instead of state.()
{:done, token} -> token
{{subject, batch}, token} -> {create_job(subject, batch), token}
end
end
def next({next, rest, batches, acc}) do
next_batch(next, rest, batches, acc)
end
defp next_batch(_current, [], [], []) do
done_tuple()
end
defp next_batch(_current, [], [], [[next | rest] | batches]) do
{{next, rest}, {next, rest, batches, []} # instead of fn -> next_batch(next, rest, batches, []) end}
end
# and similar
This provides similar benefits of encapsulating the continuation state, but makes it more debuggable. Similar approach is used in things like :ets.select/1
for batched traversal of the ets table.