In a recent blog post, I talked about things like promises in Pony that help facilitate the safe, race-free, non-blocking development style that is one of the hallmarks of Pony. Promises are just a means to an end: the hard part has been switching my mental model from functional/imperative to the actor model.

In PonyMUD — a multiplayer text adventure game that I’m writing as a means to teach myself Pony — players have access to a command called who . This command displays the list of all connected players.

If I were to write some psuedo-code to solve this problem using the mental model I’ve had since I started programming 30 years ago, I might end up with something like this:

for player in players do

_conn.write(player.getname() + "

")

end

_conn.write("There are " + players.size().string() + " players connected.

")

This looks pretty straightforward, as it should. Solved in the traditional fashion it’s a brain-dead simple problem. First, we iterate through the player list. For each player, we display their name and then when we’re done, we display the total number of players.

But what if players were actors? What if you couldn’t synchronously query state? If a player is an actor in Pony, you can’t get an answer from the getname() function. Actors are required to only have one-way messaging behaviors.

In my previous blog post, I talked about how the who command is implemented, but I didn’t really discuss that everything in the chain from player input to output to the player’s socket is a uni-directional, non-blocking message sent through actors.

The flow goes something like this:

Player types “who”

ConnectionManager gets text via a TCP notification

gets text via a TCP notification Player actor parses text and dispatches to appropriate handler actor

actor parses text and dispatches to appropriate handler actor CmdWho actor sends promise to ConnectionManager

actor sends promise to ConnectionManager iterates over players to fulfill promise

iterates over players to fulfill promise CmdWho defines what happens after promise is fulfilled

defines what happens after promise is fulfilled Player actor sends text to socket

The important thing about this flow is no part of it is blocking. There isn’t a single thing that happens in response to a user entering text into the game that “sits and waits” synchronously. While the impact may seem minimal on a text adventure game, the ability to model your process this way in performance-constrained environments is incredibly powerful. When you’re handling hundreds of thousands or millions of requests per second, every blocking operation could be the death of your process performance.

With the release of Pony 0.17.0 I’ve actually been able to delete some of the code I wrote in my previous blog post and replace it with some more elegant use of the promises library.

Let’s take a look at the important methods of the CmdWho actor:

be handle_verb(verb: String val, params: Array[String] val) =>

let p = Promise[Array[String val

] val]

_cm.who(p)

p.next[None](recover this~_whofulfilled() end) be _whofulfilled(players: Array[String val] val) =>

_parent.tell("

==> Player Listing:

")

for name in players.values() do

_parent.tell(name + "

")

end

_parent.tell("

There are " + players.size().string() + " players connected.

")

When the command dispatcher matches a player-entered verb with one of the verbs CmdWho handles, it invokes CmdWho 's handle_verb function. This in turn sends a promise to the connection manager. This is a promise that indicates that at some point in the future the connection manager will give us back an array of strings containing the names of connected players.

When we get this fulfilled promise, we invoke _whofulfilled (via next promise chaining), which just runs through the array and sends text to the player’s socket via the _parent.tell behavior (remember — everything is an an actor here).

Because the connection manager is the only thing with a raw list of player actors, it is the only actor that can fulfill the promise to get their names. Since players are actors and we can’t synchronously query their names, we have to ask for the name with another promise. This is where the new join method from 0.17.0 makes the code very simple:

be who(p: Promise[Array[String val] val]) =>

let names: Array[Promise[String]] = Array[Promise[String]]

for player in _players.values() do

let pname = Promise[String]

player.gathername(pname)

names.push(pname)

end

let joined: Promise[Array[String val] val] = Promises[String].join(names.values())

joined.next[None](recover p~apply() end)

First, we create an array of promises, all of which have been sent to the player actors to obtain the name of that player via the gathername behavior.

Once we have an array of promises, we can convert that into a promise of an array by using Promises.join on an iterator of promises. When the joined promise has been fulfilled, we then invoke apply on the promise sent to the connection manager. Fulfilling one promise in response to fulfilling a nested promise may feel a little awkward at first, but I am really excited by the potential and power of this pattern.

In conclusion, modeling everything as asynchronous actors that can only send one-way messages is definitely a paradigm shift. It takes some time and a lot of extra thought, but the end result is, I think, worth it.

While the initial conversion is a struggle, I’ve found that the problem decomposition that I’ve been coming up with in order to “actor-ify” the solution has been more elegant than if I had just brute-forced things in an easy, synchronous for loop.

Pony’s promise capabilities (especially the new syntax available in 0.17.0) make it easy to logically chain results to build complex behaviors without ever stopping to wait for a synchronous block or sacrificing memory safety.