An Actor-Model Capabilities Secure Language
What is Pony?
Pony is a strongly typed, ahead of time compiled, object-oriented, actor-model language. These are not what make Pony exceptional though. What makes Pony stand out among other languages is it’s focus on “capabilities”. Capabilities can be thought of as additional type information that form guarantees about how that data will be used. Other languages in the concurrency space require users to use patterns based on locks/semaphores. Newer offerings like Rust use “ownership” to restrict access. In Pony, the type system includes reference capabilities to ensure references (variables) pointing at the same data are compatible in a way that guarantees no data-races can take place. The compiler enforces these guarantees. If you're trying to read from data that is writable in another reference in a different thread, that program will not compile. It’s not safe. If your program does compile, it IS safe. If it does compiles, it cannot crash at run-time, and it’s free of data-races and deadlocks. This is the guarantee you get with refcaps (reference capabilities).
What are Actors?
Pony is an object-oriented language. Basic units are contained within Classes/Actors. There are no meandering variable declarations or functions external to a Class or Actor. Classes look something like:
class Dog let env: Env let name: String let sound: String = "Woof!" new create(env': Env, name': String) => env = env' name = name' fun speak(env: Env): None => env.out.print(name + " says: " + sound)
OK cool, but I thought we were talking about Actors. Well, Actors look almost identical:
actor Cat let env: Env let name: String let sound: String = "Meow!"` new create(env': Env, name': String) => env = env' name = name' be speak(env: Env): None => env.out.print(name + " says " + sound)
The main difference here is that Classes have functions (procedures) with keyword
fun, and Actors can have behaviors, keyword
be. Functions operate synchronously, whereas behaviors run asynchronously. Behaviors also have no return value as they may never return at all (although you can use promises to get asynchronous values). Every call to a behavior gets scheduled to a thread by the main scheduler. Actors, themselves, however, operate sequentially. i.e. An Actor can only call 1 of it’s behaviors at a time. So instead of thinking of Actors as a unit of parallelism, you can think of them as a unit of sequentiality. Your actor code embodies procedures that must operate in sequence.
With great power comes great responsibility
So much parallelism out of the box sounds like a recipe for corruption and data-races. This is where reference capabilities come in. So far I haven’t shown any, but they were there. All types have refcaps, although if not specified their default cap is used. There are 6 different refcaps with different guarantees: iso (isolated), trn (transitional), ref (reference), val (value), box and tag. Refcaps look just like type annotation.
let pi: F32 val = 3.14 ''' pi is a 32-bit float with val capability. this alias is read-only and it can have new aliases that are also read-only. '''
- * An iso alias can R/W but guarantees it’s the only variable that can R/W to that data.
- * A trn alias can also R/W but only guarantees other aliases are read-only.
- * A ref alias is what you'd expect from a variable in a regular language. It’s R/W and other aliases can be R/W also.
- * A val alias is read-only and other aliases can read also, but not write.
- * A box alias is read-only but other aliases can be read-only or R/W.
- * A tag alias cannot be read or written, but other aliases can be read-only or R/W.
| deny global R/W aliases | deny global W aliases | don't deny global aliases ------------------------+-------------------------+-----------------------+-------------------------- deny local R/W aliases | iso | | deny local W aliases | trn | val | don't deny local aliases| ref | box | tag | (mutable) | (immutable) | (opaque)
In order to guarantee an alias is safe to be shared concurrently, it must either guarantee:
- * Either no object can write to the object, in which case, any actor can write to it
- * Only one actor can write to the object, in which case, no actor can read or write to it.
Reference capabilities help the compiler verify our data is safe.
Refcaps are crazy powerful, but there’s more. All programs are initialized with a similar pattern: an Actor named
actor Main new create(env: Env) => None
Main takes, in it’s constructor, a special object called
Env which contains several Object capabilities. The docs say:
[An object] capability is an unforgeable token that (a) designates an object and (b) gives the program the authority to perform a specific set of actions on that object.
Basically, at start up, your
Main program is bestowed certain rights. Like rights to IO, or rights to network sockets, or rights to access the filesystem etc. It receives this in the form of globally unique primitive objects. Because of Pony’s memory safety guarantees, you can’t steal or copy this token, you can only receive it explicitly as a function/constructor argument. Consider
Main as the school principle, and object capability tokens are like hall passes. If you don’t have a hall pass, you can’t do any of the things under that tokens responsibilities. This ensures xyz 3rd party library that’s only supposed to add ANSI colors to your terminal output isn’t phoning home over http, or reading your secrets/config files if you didn’t explicitly give them permission to do so. That’s big.
As an additional parallel, you can imagine Android’s permission model. Apps need to require specific permission to do certain actions on your device. If a library requests more capabilities than you feel like it needs, you're encouraged to not use that library.
Here’s an example from an http framework, Jennet:
actor Main new create(env: Env) => let tcplauth: TCPListenAuth = TCPListenAuth(env.root) let fileauth: FileAuth = FileAuth(env.root) let server = Jennet(tcplauth, env.out) .> serve_file(fileauth, "/", "index.html") .serve(ServerConfig(where port' = "8080")) if server is None then env.out.print("bad routes!") end
To serve static files, Jennet requests access to the TCPListenAuth and the FileAuth. If it didn’t have them, it wouldn’t be able to perform those functions.
In Pony, arithmetic comes in many flavors. By default, Pony looks out for overflow/underflow and division by zero at a small cost to runtime performance. You can optionally use modified operators i.e.
+~ to do “unsafe arithmetic”. You can increase performance at the cost of safety (and you void your crash free warrantee). There are 2 more variants available – checked and partial arithmetic. In the partial variant – written
+?, Pony raises an error on overflow/underflow and division by zero. Not this is an “error” not a crash/exception. If you're program can raise an error, the compiler will tell you that you must handle it or it will fail to compile. The last variant – checked – is used in method for like
addc() returns a tuple on which the second argument is
true if there is overflow/underflow or division by zero. On top of this, Pony allows operator overloading by redefining an objects default arithmetic methods.
Beyond these outstanding features – Pony is a very capable language with the modern niceties you may have come to expect in a language.
- * Pattern matching/destructuring on value/type and refcap.
- * A well designed C FFI allowing you to interface with C from Pony
- * Builtin unit testing and property-based testing libraries
- * Generic types
- * A full-featured standard library
TL;DR: Pony is an exceptional language that can guarantee freedom from run-time crashes, is strongly typed, memory-safe, concurrency safe without locks or deadlocks and is still a pleasure to read and write. Head on over to https://www.ponylang.io/discover to learn more about this awesome language.