Elixir Adventures

For my Elixir Language of the Month project, I decided to return to my roots, in a way. You see, once upon a time, I wrote my first program. I had an idea for an space opera RPG, which, in retrospect, was a tacky Star Wars/Final Fantasy hybrid. Still, I wanted to bring that idea to life in any way I could; thus, I spent a good chunk of time my sophomore year of high school spewing out a morass of IFs and GOTOs in BASIC on my trusty TI-83+. Eventually, I did have a working game; it was a short text-based adventure, a sort of “demo” for the full game I had in my head.

I recently thought about text-based adventures and NPCs roaming about, and it seemed to be a natural fit for a concurrent programming language like Elixir.

Here's a link to the finished code:

https://github.com/hoelzro/language-of-the-month/tree/master/April-2016

Organization of the program

So the central idea is to have a text-based village with several NPC residents; I created three and named them Alice, Bob, and Carol.

Since the whole point of learning Elixir was to teach me to think in terms of processes, I broke everything I could into processes, the relationships between which looks like this (hover a service to see a description):

You can see these relationships in the graph directly represented by the code itself:

map = read_map_from_file("map.txt")
clock = spawn(fn() -> ElixirRpg.Clock.main() end)
location_manager_proc = spawn(fn() -> ElixirRpg.LocationManager.main(map) end)
npc_manager_proc = spawn(fn() -> ElixirRpg.NpcManager.main(clock, location_manager_proc) end)
evaluator_proc = spawn(fn() -> ElixirRpg.Evaulate.main(location_manager_proc, npc_manager_proc, clock) end)
parser_proc = spawn(fn() -> parser(evaluator_proc) end)
printer_proc = spawn(&printer/0)
send(evaluator_proc, {:look, printer_proc})
reader parser_proc, printer_proc

One side effect of doing things this way is that the ideas naturally organize themselves into services of a sort; if this were a non-trivial program, it would be easy to separate them into services on different machines 1). Passing process handles to the processes the need to communicate with them also serves as an interesting form of dependency injection. Elixir allows you to bind a process to a symbol by registering it, which would bypass this idea. I neglected to register my processes, as I liked being able to control exactly who's talking to whom.

Coding NPCs

If I have a chance and desire to revisit this project in the future, one of first changes I would make would be to make changing NPCs less rooted in code. In the current implementation, the map was configured via Unicode art in a file, but each NPC needed to live in their own module, and you needed to tell the NPC manager program which NPCs were around and which locations they would go to, which was tightly coupled to the map. Admittedly, at this point in the program I just wanted to be done!

Challenges

The implementation was pretty straightforward; however, I did run into some trouble along the way.

Anybody Home?

The biggest problem was that I would sometimes run into a situation where I would type in a command into my game, and nothing would happen. As the number of processes increased, the number of possibilities for what went wrong went up. What usually happened was one of two things:

  1. Some code would request something from another service, and either a) I would send a request that I wasn't looking for in the service, or b) the service would send a response that didn't look like what the requestor was expecting.
  2. My code would “forget” to send a response to the requesting process.
  3. One of my cases in a receive would forget to continue the loop, so the service would die.

Situation #1 would happen much more often than the other two; here's what code like that looks like:

def client do
  send(service_proc, {:get_value, self()) # a) not sending what service expects

  receive do
      {:get, value} -> ... # b) not receiving what service is sending in response
  end
end

def service do
  loop = fn(loop, value) ->
    receive do
      {:get, requestor} ->
        send(requestor, {:get, :response, value})
        loop.(loop, value)
      {:incr, requestor} ->
        send(requestor, {:incr, :response, value + 1})
        loop.(loop, value + 1)
    end
  end

  loop.(loop, 0)
end

I ended up resolving these issues by liberally sprinkling msg -> raise “Unexpected ” <> inspect(msg) into my receive blocks, causing fantastic stack traces when I sent/received something I shouldn't, and by adding some after 1_000 -> raise “timeout” expressions after my receives; this helped me fix issues #1 and #2. To fix issue #3, I just made sure each service had a raise “I'm not supposed to exit” after their receive blocks. I'm sure that the Erlang ecosystem offers far more sophisicated tools for this kind of thing, especially for #3; I believe that's the kind of thing that supervisors are good for.

This raised a question with me, however: what happens when an Erlang process' mailbox gets a bunch of messages it doesn't select in receive? Does the mailbox fill up? That could make for an interesting topic of research some day!

Non-trivial Macros

I learned the hard way that non-trivial macros are a bit harder in Elixir than they are in Lisp; let's look at the example from above. I tried to make this code:

receive do
  {:get, requestor} ->
    send(requestor, {:get, :response, value})
    loop.(loop, value)
  {:incr, requestor} ->
    send(requestor, {:incr, :response, value + 1})
    loop.(loop, value + 1)
end

…look more like this:

receive do
  handle_method :get, {} do
    value
  end

  handle_method :incr, {} do
    value + 1
  end
end

The tuple in each handle_method macro is supposed to be the list of arguments that each method takes.

The first part was trying to construct the tuple that was to go on the left-hand side of -> (in the first case, {:get, requestor}. Splicing symbols, variables from macro land, and the variables in the tuple from the invocation proved to be less than straightforward. For example, let's say your macro is passed a tuple. What does that look like to the compiler?

ast = quote do: {foo, bar}
IO.puts inspect(ast) # {{:foo, [], Elixir}, {:bar, [], Elixir}}

Sometimes I would see {:{}, [], [{:foo, [], Elixir}, {:bar, [], Elixir}]}, which just made things confusing. Once I figured out that the tuple is passed in as a triple, I was able to construct the LHS tuple pretty easily, but I feel that there's a bit of a disconnect between the code and the AST, unlike when working with macros in Lisp.

Sadly, even when I got that working, my macro ended up not working at all - it turns out receive is very strict about what goes inside; so strict that my macro wasn't even called before my code was rejected. I may have been able to introduce my own receive-like special form that allows handle_method within the block, but I was macro'd out at this point!

Continuing with Elixir

Elixir was a lot of fun to work with; I definitely feel it was sufficiently brain-bending. I wouldn't mind getting to know it and OTP a little better; I didn't even get into anything fancy like supervisors. I'll be sure to keep it on my toolbelt for any future projects that lend themselves to a concurrent or distributed model!

  1. 1) The Erlang runtime has built-in functionality for running different processes on different machines, but let's pretend we're working in a concurrent environment that can't
Published on 2016-05-23