Remix.run Logo
gwd 4 days ago

OK, so having poked around the documentation a bit, I do think the Pony documentation could use a lot more examples. But there's one reasonably concrete example here:

https://patterns.ponylang.io/data-sharing/isolated-field

Basically what I gather is:

1. Actors are like threads, but have data structures associated with them. Actors have functions like methods associated with them called "behaviors", which are called asyncronously. BUT, any given Actor will only ever have one thread of execution running at a time. So calling a "behavior" is like sending a message to that actor's thread, saying, "Please run this function when you get a chance"; "when you get a chance" being when nothing else is being run. So you know that within one Actor, all references to Actor-local data is thread-safe.

2. They have different types of references with different capabilities. Think "const *" in C, or mutable and immutable references in Rust, but on steroids. The extra complexity you do in managing the types of references means that they can get the safety guarantees of Rust without having to run a borrow checker.

So in the above example, they have a Collector actor with an internal buffer. Anyone can append a character tot he internal buffer by calling Collector.collect(...). Code execution is thread-safe because the runtime will guarantee that only one thread of Collector will run at a time. The data is of type 'iso' ("isolated"), which ensures that only one actor has a reference to it at any time.

Once the internal buffer gets up to 10, the Collector will transfer its buffer over to another Actor, called a Receiver, by calling Receiver.receive(...) with its own internal buffer, allocating a new one for subsequent .collect() calls.

But its internal buffer has a reference of type 'iso', bound to Collector. How can it transfer this data to Receiver?

The magic is in these two lines:

    let to_send = _data = recover Array[U8] end
This creates a new local variable, to_send. Then it atomically:

- makes a new Array[U8] of type iso

- assigns this new array t; Collector._data

- Assigns the old value of Collector._data to to_send

Now Collector._data has a new reference of type iso, and to_send has the old one.

Next we do this:

    _receiver.receive(consume to_send)
The "consume" ensures that to_send can't be referenced after the consume call. So the compiler can verify that Receiver.receive() will be the only one able to access the old value of _data that we passed it.

Sounds like an interesting approach; it would be nice to see more examples of realistic patterns like this; perhaps simple sequential programs broken down into multiple actors, or things like a simple webserver implementation, with some sort of shared state.