aseigo

aseigo

Stupid pet tricks: functions as state

Had a little task today, and the quick solution felt a bit like a stupid pet trick … thought I’d share it here as something a little different from the continual stream of questions :wink:

The task was this: we needed all the pair-wise combinations from a set of entries, and these need to (for performance reasons) be batched up. So we have data like this:

[1..10_000]

… which gets turned into batches like this using Enum.chunk_every/2:

[[1..500], [501..1000], [1001..1500], .. etc]

We then want to do some computation with the next value (e.g. 1) against the rest of the values in its batch (e.g. [2..500]) and then subsequently against each further batch (e.g. [501..1000], then [1001..1500], etc). Pair-wise combinations. Fun.

The computation is done async and the batches are generated on request by a GenServerfor consumption by workers. So we need to keep track of where we are in the batching, so we need to keep state. Yuck! State! amiright?

Instead of keeping the chunked data around in a state term, I instead opted to keep a function there instead which captured that data:

batches = Enum.chunk_by(sequence, batch_size)
state = fn -> next_batch(batches) end

It is then used like this from a message handler in the GenServer:

def next_job(state) do
  case state.() do
    {:done, _} = done -> done
    {{subject, batch}, next} -> {create_job(subject, batch), next}
  end
end

What is that next_batch call in initial state term, you ask? (Ok, you probably didn’t .. but, then again, maybe you did since you have read this far!)

defp next_batch([[current| rest] | batches]) do
  next_batch(current, rest, batches, [])
end

defp next_batch(current, [], [], []) do
  done_tuple()
end

defp next_batch(_current, [], [], [[next | rest] | batches]) do
  {{next, rest}, fn -> next_batch(next, rest, batches, []) end}
end

defp next_batch(_current, [next|rest], [], acc) do
  {{next, rest}, fn -> next_batch(next, rest, acc, []) end}
end

defp next_batch(current, rest, [next_batch | batches], acc) do
  {{current, next_batch},
    fn -> next_batch(current, rest, batches, [next_batch | acc]) end}
end

defp done_tuple(), do: {:done, fn -> done_tuple() end}

Generators! Ignoring the moderately ugly function headers, the useful bit is that the functions return a tuple containing the result of the calculation (the next batch) as well as an anonymous function that contains the next call to next_pair_job that can be used to get the next job … this allows the “detail” of how next_batch/4 works to be entirely opaque to the calling code.

It calls the function (which is its state!) until it gets a :done tuple. It doesn’t matter how many times it is called as the :done tuple has a function which, when called, itself returns the same :done tuple. As this is used in a distributed application where we can not know the order or number of calls in advance, that’s a necessary attribute to have, and the above manages that elegantly. :slight_smile:

It was just a nice way to encapsulate the actual iteration through the batches so it could be “hidden” from the GenServer using it without cluttering up its own message handlers. Performance was just fine, so the code cleanliness this approach offered was considered to offset the overhead.

During code review it came up as an out-of-the-ordinary approach (though certainly not novel), so thought I’d share it here. Yay, stupid pet tricks! :slight_smile:

Most Liked

peerreynders

peerreynders

It seems the real culprit for containing state isn’t mentioned: closure.

Returning state via a function closure, for example, is at the core of implementing trampolines in JavaScript in order to implement stackless recursion.

And then there is this (2003):

  • Objects are merely a poor man’s closures
  • Closures are a poor man’s object
michalmuskala

michalmuskala

It’s not entirely true in Elixir because of one thing - you can’t mutate data in the closure. It’s entirely true in runtimes with mutable data.

michalmuskala

michalmuskala

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.

Where Next?

Popular in Discussions Top

blackode
Elixir Upgrading is so Simple in Ubuntu and It worked for me Ubuntu 16.04 git clone https://github.com/elixir-lang/elixir.git cd elixir...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
Fl4m3Ph03n1x
Background A few days ago I was listening to The future of Elixir from Elixir Talks, with Dave Thomas (@pragdave ) and Brian Mitchell. I...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
sashaafm
Piggy backing a bit on @dvcrn topic BEAM optimization for functions with static return type?, I’ve been trying to understand in a deeper ...
New
mmmrrr
Just saw that dhh announced https://hotwire.dev/ Is it just me or is this essentially live view? :smiley: Although I like the “iFrame-e...
New
praveenperera
How We Replaced React with Phoenix By: Thought Bot
New
New
Owens
Hello all, I am developing a new mobile app with Flutter frontend and Phoenix backend. The mobile app has real-time task management and c...
New
paulanthonywilson
I like Umbrella projects and pretty much always use them for personal Elixir stuff, especially Nerves things. But I don’t think this is ...
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36128 110
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 47930 226
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New

We're in Beta

About us Mission Statement