Stupid pet tricks: functions as state

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.

4 Likes