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 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 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 . 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:
- 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.
- My code would "forget" to send a response to the requesting process.
- 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 receive
s; 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!
Published on 2016-05-23