I wanted to study some twenty-first century programming languages, open source and not coming out from a big vendor, and I picked Rust and Pony. Here is my potted overview of the features.

Both Pony and Rust are compiled programming languages for highly concurrent applications, use C family syntax (like C++, C#, Java), are designed to be safe (thread-safe, deadlocks, race conditions, dangling pointers). But both add quite a few nice wrinkles and side ideas. 

I think the big picture is that—

  • Pony is an object-oriented language very suitable for Agent-oriented programs. You might see it as a replacement for C++ with thread-safe references. Sean Allen suggests Pony is good for “highly concurrent programs where you are manipulating state and want to make sure you don’t mess it up”  Pony’s sweet spot may be when you have a lattice or tree or array of intercommunicating agents.
  • Rust is imperative and only kinda object-oriented and kinda-functional, comparable to C performance in benchmarks, and is very suitable for libraries and back-end applications, but not for applications with GUIs.  You might see it as a replacement for C with safe pointers.  Rust’s sweet spot may be when you need concurrent processing on a large array, where each thread takes on a slice of the array data to process.

 

Feature Rust Pony
Compiled Y Y
Garbage collector N Y
Procedural Y Y
Functional Y Y
Object-Oriented Y-ish Y
Concurrency Threads Actors
Strongly-typed Y Y
Generics Y Y
Traits Y Y
Closures Y Y
DSL Y Compiler plugins N
Macros Y N
Exceptions N use Result Y untyped
Automatic resource finalization function Y define drop() Y define dispose() and use with
Null N use Option N use None
Memory Protection Lifetimes and mutability Reference capabilities
Memory Safety Y Y
Thread Safety Y Y
Basic Function Overloading Y Abstract trait N Case functions
Mutable variables N as default Y as default within actor
Partial Application N Y
Compile-time evaluated functions Y N
Compile-time metaprogramming Y macros N
Runtime metaprogramming Limited. External use Cargo
for dynamic compilation and load
N
String Encoding UTF-8 UTF-16
Array Slicing Y N
Case/match statement with patterns Y Y
Labelled break and continue Y N
Assertions Y built-in N
Unit testing Y Built-in N use PonyTest
Foreign function interface C not C++ C not C++
Inline assembler Y N

 

A Trait is like a class that can be mixed-in, but does not have any fields. It allows different abstract or real functions to be defined, and for providing type information to satisfy generic functions implemented with that type as an argument.

Memory

Both Rust and Pony are designed around compile-time features to prevent memory problems: how do you make sure your program has enough details that static checking can detect errors and fail to compile.

  • Pony’s approach is to try to limit the read and write permissions for a variable (variable binding) for the current and external actors.
  • Rust does it by limiting the number of variable bindings to a variable that can write to the variable data (on the heap) at any one time to one only.

Pony

The operation for Pony is that a variable can be one of six capabilities:

  • iso = isolated. You only have one reference in the current actor, so you can give it to another actor
  • val = immutable by the current or any other actor
  • ref = read and write by current actor, not shareable or accessible by other actors
  • box = read-only for the current actor
  • trn = transition.  Variable can change the capability
  • tag = tag. Store and compare operation only

In your normal development within a thread, you probably would use just val and ref capabilities, which is not too difficult to think about. The others come into play when you want to communicate with other actors. Using these tags, the Pony compiler can find errors or generate appropriate code for safe operation. Their bottom line: “By sharing only immutable data and exchanging only isolated data we can have safe concurrent programs without locks.”

Rust

The operation for Rust is that if you have multiple references to an object, only one at any time can write to it or “owns” it. And to make that easier, Rust has several different ways to refer to an object:

  • Local Variables.  Variables defined using var are local variables and act pretty much how you might expect. They go out of scope and the underlying resource is reclaimed when the current {} scope exits. They can be assigned to multiple times.
  • Variable Bindings. Every allocated resource has a single variable that is bound to it: that variable owns it.  The ownership can be passed through a succession of different variables, and the assignment operator = is used for the purpose.  When the resource is bound to a different variable, the former variable binding becomes unavailable. Only the owner can write to the resource.
  • Variable References. If a variable name is used preceded by & then what is passed is a reference to the resource. This is called “borrowing” the resource. When the bound resource goes out of scope, then the reference cannot refer to it, and a compile time error will be flagged; to cope with this, there is a mechanism called lifetimes to prevent a resource from being disposed by tieing it to the scoped lifetime of containing variable.

So far so good. However, what happens next is that Rust has a bizarre set of default behaviours for the variable bindings and variable references,  to the extent that while Rust has C syntax, you cannot really say it has C semantics as far as = is concerned: you need to interpret what is going on very differently.

  • Variables defined with let are immutable by default, unless you provide the mut keyword. So the following will generate a compile-time error:
let x = 7;
x = 8;       // Error!  Default is single assignment
  • Similarly, variable reference need  &mut to be mutable.
  • For a variable binding, assigment really is a move of ownership. So the following will generate a compile-time error
let v = vec![1, 2, 3];         // bind variable v to a new vector
                               // using macro vec!
let v2 = v;                    // move ownership from v to v2
println!("v[0] is: {}", v[0]); // Error!  v is not bound to anything
  • The resource that a variable is bound to will be disposed of when the current scope of that variable exits. So if the variable has been used in something that has a longer scope there will be a compile time error. Here is another example from the Rust book:
struct Foo {
    x: & i32,
}

fn main() {
    let x = 5; 
    let y = &x;
    let f = Foo { x: y };
    ...
    println!("{}", f.x);  // Error 
}

Why is this an error? Because if there is a reference made to f and passed around, the underlying foo data for f could last longer than the y variable. So there needs to be a way to detect if there is chance that the reference to f could live longer than the resource for y.  To do this, there is syntax for linking the lifetimes of variables and structures:

struct Foo<'a> {
    x: &<'a> i32,
}
These lifetimes show up in a few different places. To learn Rust, you have to get to grips with this unique behaviour, which certainly add a syntax tax on non-parallel programs.

The upshot of these is that it is a little more complex to write even fairly simple code: you have to have to state how a variable will be used when you declare it. Rust’s Graydon Hoare has said he thinks Pony looks like an early version of Rust, and that the cognative load of those capability keyword is too much, but I don’t see how Rust is not worse, in that regard, frankly.

Other Considerations

  • All modern GUI systems and libraries are based on classes and inheritance. Rust is not an object-oriented system. You would have to go through hoops to make an FFI interface to a window system in Rust, and you end up with a complex system with none of the flexibility of classes: so Rust is suited for backends, computations, system programs, generating HTML, but not for the parts of desktop applications that need to speak directly to GUI APIs.  (I expect the same is true for Pony.)
  • What Rust has instead of classes is traits and macros which are LISP style AST macros not cpp style textual macros.
  • Rust does not have exceptions. It has a panic macro, to halt the show, but the idiom it encourages is for functions to return a Result type, a struct which combines the result with an OK or Err enumeration.
  • There has been suggestion that, due to the way that garbage collection works, it is better to structure a large non-threaded program a sequence of actors.
  • Rust has a secret weapon: the cargo tool is a commandline application that wraps up the compiler, builder, package manager, downloader, dependencies, unit tester, and assertion enabler in a single applications. Cargo is fantastic, really making life easy.

Rust comes from Graydon Hoare at Mozilla, now working on Apple’s Swift, and small bits of Firefox are written in Rust.  Rust seems to have an active community, a good amount of IDE and compiler support.  Pony’s original developer Sylvan Clebsch has been poached by Microsoft, but the language and community is active perhaps at a reduced rate.