My First Day with Elixir

Kevin Hoffman
4 min readOct 22, 2017

--

While I still spend most of my time on production-bound services written in Go, I like to experiment and tinker and I have a compulsion to continually learn new things. This is what led me to start learning Rust earlier this year and how I’ve stumbled onto Elixir.

I had a project idea for something that wasn’t critical path and so I decided to try and build it in Elixir. I need to monitor some messages coming off of a subscription channel, aggregate that information, and make it available in a web UI.

I created a new Phoenix project and got to tinkering. The first thing I tried to do was create a process that receives messages from the subscription. This took a long time (by long, I mean 2 hours). I experienced quite a bit of “culture shock” swapping from Go to Elixir.

Figuring out how to get worker processes started up by using the children array in the Phoenix application’s start/2function was annoying. The documentation is full of samples of how to create a named process, but nothing shows me how to create a named process using the worker/2and supervisor/2functions. This is a pretty consistent problem among nearly all programming languages — The introductory content is largely out of context and provides you with a thousand isolated techniques that will only coalesce into a meaningful base until after you’ve suffered through needless frustration. I had this problem with Go, Rust, and Elixir. But again, we’re only talking about a few hours, so perspective is important here.

The next problem I had was with the receive macro. I tried to create an infinite loop of message receive pattern matches, but this of course blocked everything and prevented the Phoenix server from starting. Turns out I needed to do Task.start_link( fn -> start(subscriber_pid) end) inside the start_link function of my worker. This allowed the receive block to run in a different PID than the worker, keeping things unblocked. It also took me a while to figure out that the function that listens for subscribed messages didn’t need to be a GenServer. Figuring out what does and doesn’t need to be a GenServer feels like it could be something that takes multiple “ugly projects” to learn from to figure out the right balance. When do I use a GenServer versus just spawning with Task.start_link or maybe even just spawn or spawn_link? This’ll take me a while to get my head around.

Once I got the worker process to subscribe to messages, then it was time to build a GenServer to hold the message aggregates. This is where things got ugly and what I can only imagine is hideously non-idiomatic Elixir:

def handle_cast(heartbeat, heartbeats) do
newmap = if !Map.has_key? heartbeats, heartbeat.service_id do
Map.put(heartbeats, heartbeat.service_id, %{})
else
heartbeats
end
newmap = put_in(newmap[heartbeat.service_id][heartbeat.instance_id], heartbeat.timestamp) {:noreply, newmap}
end

Here the messages to which I’ve subscribed are heartbeat messages that have a service ID and an instance ID and I’m creating a nested map. The root key is the service ID and the key of the nested map is the instance ID. This allows me to store the most recently received heartbeat for each instance.

This whole thing looks like it’s begging for a refactor into something more idiomatic. I don’t like the presence of the if statement, but the put_in function doesn’t work if you can’t fully traverse the path of keys to the value you’re inserting. This left me wondering if a nested map was the best choice of data structures, or if there’s some elegant solution here that I just don’t see because I lack a broad enough frame of reference?

And then there’s this code that grabs a message from the raw subscription source, converts it to JSON, and sends it to the GenServer that’s hosting the code in the preceding sample:

defp handlemessage() do
receive do
{... pattern for msg ..} ->
{:ok, hb} = Poison.decode(msg, as: %MyStuff.Heartbeat{})
MyStuff.HeartbeatCache.put(:heartbeats, hb)
end
handlemessage()
end

I feel like I should be able to do something more elegant than what I’ve got, something that uses the pipeline operator like this:

msg
|> Poison.decode as: %MyStuff.Heartbeat{}
|> MyStuff.HeartbeatCache.put(:heartbeats)

But this doesn’t work for a number of reasons. First, the Poison.decode function uses a named parameter called as: that I can’t seem to finagle into a use of the pipeline operator. The second is that my heartbeat cache put function requires the pid of the cache as the first parameter, and the pipeline operator always sends the result of the previous pipe as the first parameter to the second. I’m struggling with rationalizing functions that return tuples with the use of pipelines.

I feel like if I wasn’t a hopeless n00b, I’d be able to make the “take a message off the subscription source, transform it, and stick it in the cache” operation look smooth and Elixir-like.

So, after all this I feel like I need to point out that I do have working code that does what I want it to do, only a few hours after starting. I literally installed Elixir two days ago. I can’t remember the last time I was this productive this early in a programming language. Time will tell what the shape of the learning curve looks like as I go from here to a finished project.

--

--

Kevin Hoffman
Kevin Hoffman

Written by Kevin Hoffman

In relentless pursuit of elegant simplicity. Tinkerer, writer of tech, fantasy, and sci-fi. Converting napkin drawings into code for @CapitalOne

Responses (1)