Before you turn away from this blog post groaning about the bad pun, you should know that there’s actual content here. I just couldn’t resist the urge to title a post about promises with a bad pun.
Every once in a while when I get some spare time, I go back to tinkering with Pony as a way to keep learning and also keep my perspective fresh. This last tinkering session revolved a lot around the idea of promises.
Before diving into my specific problem, here’s a quick example of a promise in Pony in case you haven’t played with them yet:
let p = Promise[String]
The promise is a contract that at some point in the future, the expected value will either be fulfilled or rejected. By using the apply method in this sample, we’re fulfilling the promise. Promises actually represent a very small amount of code in the stdlib because promises are actors. So when you fulfill a promise, you’re actually invoking an async behavior.
What do we do with the value of a promise once we have it? We can use another promise to deliver that value however we like:
let p = Promise[String]
Here we’ve used partial application to tell the promise that once fulfilled, it should take the value of the promise (in our case, a string) and send it to the
grab function in the
valuegrabber object as the first parameter. The
~ syntax indicates partial application (or currying depending on your vernacular). We can chain together promises that return values of more than just None. This creates some very powerful scenarios that feel to me like a kind of asynchronous, actor-based “fold left” that doesn’t require the accumulator to be of a single type. If you’re familiar with Elixir, it feels like the promise chaining syntax in Pony is begging for an operator like the pipe (|>).
Now let’s get into the specific problem I needed to solve.
Pony is an actor-based language. The main entry point to your application is an actor. You can choose to have 100% synchronous interactions from there on out, but you’d be missing a huge opportunity. The tricky part with actors in Pony is that you can’t ask questions. Pony actors do not model the request/response pattern. In other frameworks like Akka, they’ve abstracted away the “send and await reply” concept into the ask pattern. Pony doesn’t have this and I personally believe it doesn’t need it.
So how do you get data out of an actor? In the experimental MUD I’m using to play with Pony syntax, how do I iterate through a collection of
Player actors, ask them each their name, and then display them to an administrator that typed the
This is far trickier (and more fun!) than I expected. I could send each of the player actors a message telling them to send their names to another actor, but that would likely require me to dive into state machines, otherwise the target actor would be like “Why am I suddenly getting names? What do I use these for? WTH?”
I then thought maybe I should separate the state from the actors… create a giant, global “state bag” that has the properties of every game object. As odd as this might sound, it’s actually not all that weird a pattern in game design where state is held or cached in a single process.
The idea of global state bugged me, because I really like the encapsulation of not exposing internal state to the rest of the game. If I went the “state bag” route, I’d have to put locks and permissions and visibility rules on it, and I could see that behemoth taking up too much of my development resources.
Further, I want the actors to maintain control of how they fulfill a promise. For example, what if a player is under a spell where they have been transformed into an anonymous newt? They should appear in the
who list as
A newt until they get better.
This is where promises came in — more specifically, promise chaining. With this, I can model the promise of an actor name, send it to an actor for fulfillment, and then when it’s fulfilled, send the result somewhere else … all without ever blocking. This is almost everything I need.
This is still kind of like the first option. I need something that is aware of the fact that I’m collecting a finite set of results, and to do something when I’m done with that set. If Pony had the concept of a PromiseCollection with chaining that operated on aggregates, this would be automatic. As it is, I only needed to write a small amount of code to take care of this.
Here’s what I want to do in my
ConnectionManager actor (this is the bridge actor responsible for creating player actors):
be dowho(requester: Player tag) =>
let collector: Collector[String] = Collector[String](_players.size(), requester)
for player in _players.values() do
let p = Promise[String]
p.next[None](recover collector~receive() end)
For every player connected, I’m creating a promise that will eventually contain their name. Once fulfilled, each of these promises is going to invoke the
receive method on my
_collector object (I’ll show that shortly). Once the collector is full, it’s going to send the full array of player names wherever I’ve deemed appropriate. In my case, this is to a function that will create a pretty string out of those player names and send it to the TCP socket of the player that typed
The collector is essentially a promise buffer where I gather until I reach a maximum number of fulfilled responses:
be receivecollection(coll: Array[A] val)
actor Collector[A: Any #send]
let _env: Env
var _coll: Array[A] iso
let _max: USize
let _target: CollectionReceiver[A] tag
new create(env: Env, max: USize,
target: CollectionReceiver[A] tag) =>
_env = env
_coll = recover Array[A] end
_max = max
_target = target
be receive(s: A) =>
if _coll.size() == _max then
_env.out.print("collector is full")
let output = _coll = recover Array[A] end
This collector works on any kind of data you can send (because that data has to come in through an actor behavior parameter). An improvement I can make is to set a timer as soon as the collector is created and if it times out before reaching capacity, it can invoke a different method on the collection receiver to indicate failure. It’s also worth nothing that I’m using structural typing for the collection receiver, allowing more flexibility in determining what kind of actor can get receive collections. Even though this is structurally typed, the Pony compiler will reject any attempt to compile that involves sending this message to an actor that can’t handle it.
Here’s a sample implementation of a
receivecollection behavior (here
_conn is the player’s TCP connection…which is also an actor):
be receivecollection(coll: Array[String] val) =>
_conn.write("\n==> Player Listing:\n")
for name in coll.values() do
_conn.write(name + "\n")
_conn.write("\nThere are " + coll.size().string() + " players connected.\n")
The single hardest part of building this was not the use of promises, it was in figuring out how to get the constraints set right when building the collector. If you look back at the implementation for it, there’s pretty heavy use of
recover. These are crucial aspects of learning Pony and they still feel a little like “black magic” to me. I bugged folks on IRC for help and inferred some things from compiler error messages, but I suspect that reliably predicting when and where to use
recover happens later in the learning curve, much like figuring out when/where
channels are a good idea in Go or learning how to satisfy Rust’s borrow-checker.
The big take-away is that you cannot send anything that has an alias pointing to it, and
consume destroys aliases, so working with actors is likely going to require using this syntax a lot.
For my actual game (source code will be in github eventually), I plan to create a collector of
ConnectionStat objects so that I can create “normal” and “admin” versions of the
who list, so admins will see things like connection origination IP, timestamps, etc, and regular users will see general references to idle times.
Takeaways from this exercise:
- Promises are incredibly useful and powerful
- It requires a different design mindset to work around the “limitation” of not being able to ask actors for their data/state.
recoveris essential to building more advanced solutions in Pony.
- Converting a type-specific class into a generic class is non-trivial, and requires a lot of attention to things like sharing, sending, and storing fields.
Finally, lest you think I’m bluffing about all this, here’s an example from the MUD where I’ve got two people telnetted in locally:
Sauron typed 'who'==> Player Listing:
KevinThere are 2 players connected.