ryanford.dev

Pony Tldr

Submitted by: Ryan Ford
Published: Jul 28, 2022 - 17:23 PM
Updated: Jul 29, 2022 - 09:08 AM

Pony Lang

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.
'''
                        | 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:

Reference capabilities help the compiler verify our data is safe.

More control

Refcaps are crazy powerful, but there’s more. All programs are initialized with a similar pattern: an Actor named Main.

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.

More safety

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.

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.


Changelog