Foreword: I’m still struggling to understand Pony’s capabilities and everything below may be just wrong. Also, this article does not replace the tutorial.
For a course on programming languages, we have decided to learn the Pony language. Learning Pony has been something really pleasant and easy until I came across capabilities. Looking for an easy and simple way to understand them, I found this. I call this “The deny properties matrix from hell”. Not because it’s evil, but because I find it really hard to learn from that diagram and also because it’s evil.
Ok, so I know this denying capabilities stuff is very formally correct, but it’s too hard to understand. So I re-wrote it thinking about what you allow instead of what you deny, and everything started to make sense. Of course, you can scroll down and see it right now but the idea of this article is to -sort of- deduce it so you can see how obvious and logical everything is.
About iso and the point of all this
Two threads can’t just share data: writes can happen simultaneously with reads and things get corrupted pretty easily. For two threads to share data, you either:
- Only allow both threads to read
- You allow full access to shared data in turns: only Thread A or Thread B can write and read from at a time.
Which means: you either share immutable data or pass mutable data. Traditionally, passing data is done with locks and mutexes. In pony, this is done with iso.
The whole point of pony is that actors send and receive messages, but the content of those messages must be either:
- opaque: something you can’t read nor write to, only identify it. (tag)
- immutable. (val)
- mutable but isolated: If you send the thing you can’t EVER read from or write to it again. (iso)
What’s cool about this is that you don’t need to copy stuff when you send messages. In other languages, to send data to another thread you either:
a)copy everything you are passing in the message to be sure that the other thread doesn’t touch your data.
b)put some locks here and there
In pony, that need vanishes because of reference capabilities.
So you can share mutable data with iso without copying it and with no mutexes. That’s the point of all this.
The security scale
This is sort of obvious, but let’s say it:
We’re going to talk about reference capabilities, so just forget about the “No access” part.
Here comes the matrix
In most papers and talks about Pony, “local” means this thread/actor and “global” means other thread/actor. This is the same matrix as before, just in terms of what you are allowed to do through the reference instead of what you can’t do.
Iso and trn are problematic, because as said before it’s unsafe for two threads to write to the same data, and for a thread to read data which can be written to by other thread (you can get a data structure that is half-updated!). So iso and trn only make sense if they are unique references. This means that only one actor has access and data never gets corrupted.
iso, val and tag are the only sendable reference types because what this actor can do to the references is the same as what the other actor can.
Here comes the Hasse diagram
Capability subtyping, from the tutorial, establishes which reference types can be assigned to which. You should read that. Here is the Hasse diagram:
Note that the Hasse diagram also shows transitivity: box is safer than val and tag is safer that box so tag is safer than val. This means you can:
var number_val:U32 val = 5
var number_tag:U32 tag = number_val
However, you can’t just:
var iso_thing:Thing iso = ….
var ref_thing:Thing ref = iso_thing
because of the uniqueness of iso.
See how it makes sense
Here I tried to put the hasse diagram on top of the matrix. See how you deny one thing at a time?
iso only makes sense thanks to consume
I mentioned how you can’t
var ref_thing:Thing ref = iso_thing
this is because of the uniqueness of iso: by performing that assignment, you create a new reference to iso_thing so it’s not isolated anymore. Instead, what you must do is:
var ref_thing:Thing ref = consume iso_thing
Consuming iso_thing sort of destroys the reference: you can’t use it anymore, it’s a compile-time error!
recover lets you change the reference type (in the other direction)
With assignments and consume you can only go downwards (I mean down the matrix: from iso to ref, from val to box and such). However, recover blocks let you go upwards (e.g. from ref to iso).
In fact, you can also go downwards and right (just like with assignments)(Thing.create() returns a ref)
var iso_thing:Thing iso = recover Thing.create() end
var val_thing:Thing val = recover Thing.create() end
recover will by default return the topmost reference type (iso, val, tag), which is all you need anyways. You can also specify a target reference type, for example:
var trn_thing:Thing trn = recover trn Thing.create() end
recover isn’t cheating
Inside the recover block you can only access iso, val and tag references from the scope, which ensures thread-safety, as discussed in “about iso and the point of all this”.
So no, it’s not cheating.
I hope this was useful. Please point out any mistakes in the comments or e-mail me.