logo

Pony Programming Language

Posted on: 2023-02-12

Conclusion

Pony is a solid offering. It has great documentation. It is well thought through and pragmatic. It introduces new ideas - in particular reference capabilities. Has strong support for actor based programming. Lots of thought has been put into being safe and efficient. It has support for object capabilities such that permissions to be able to perform actions that impact other state, such as via "ambient authority".

The part that gives me pause is that reference capability system works at perhaps at too fine level of granularity, of variables or fields. The implication is that a field variable could be shared across threads. Doing so implies each field must be dynamically allocated and tracked based on usage. It's not 100% clear why but it does seem as if struct members are dynamically allocated by default (this can be side stepped with embed). I wonder if this is why.

Overall though I think there is a lot to be learned here.

Good

Interesting

Bad

Ugly

Notes

All types start with a capital letter.

Do all types start with a capital letter? Yes! And nothing else starts with a capital letter. So when you see a name in Pony code, you will instantly know whether it’s a type or not.

Uses var and let in the usual way. Also has embed. Uses _ prefix to indicate private.

Supports names with ' as prime.

Uses the name : type style.

Doesn't use {}. Is it using indentation to indicate "containment"?

Functions and methods introduced with fun.

If mutability is wanted can have a method that is ref.

fun ref set_hunger(to: U64 = 0): U64 => _hunger_level = to

Assignment is an expression. It returns the old value. This is known as "destructive read".

Primitives...

Primitives are quite powerful, particularly as enumerations. Unlike enumerations in other languages, each “value” in the enumeration is a complete type, which makes attaching data and functionality to enumeration values easy.

Actor are like classes, but consist of behaviors, which are like methods, but do not exectute immediately when called. They exectute at some future point, and so can't return anything. Behaviors are introduced via be. Behaviors on an actor are only executed one after another. This means there is no synchronization issues.

Has both nominal and structural types. Interfaces are structural types. Meaning if a class implements all of the methods required by an interface it can be used as that interface. Also has trait which requires specifying it is implemented. Interestingly it is also possible to use is to specify an interface is implemented. Doing so allows default implementations, which doesn't make much sense with interfaces default "duck typing" behavior.

struct exists to communicate through FFI.

An embed field is embedded in its parent object, like a C struct inside C struct. A var/let field is a pointer to an object allocated separately.

Huh. Well that's a little worrying.

Type Aliases

primitive Red    fun apply(): U32 => 0xFF0000FF
primitive Green  fun apply(): U32 => 0x00FF00FF
primitive Blue   fun apply(): U32 => 0x0000FFFF

type Colour is (Red | Blue | Green)

The enumeration "values" are acutally types (introduced with primitive). The apply thing can be avoided via sugar.

Type Expressions, seems kinda nice.

Array literals use [] but elements are separated by newline or ;. It's perhaps worth saying ; is not needed to end statements.

Will infer the type of an array, by looking at all of the types, including making it a tagged union.

Uses if then and end, like lua.

Uses [] to specify generic parameters.

All fields must have defined values. Defined as default values or set in constructors. Initialization must be performed in the constructor, not in something called from the constructor. Fields must be defined before fun or be (behaviors), otherwise they will be assumed to be local variables to the fun/be.

Embed can be used on classes and structs. It removes the pointer indirection, but may impact garbage collection.

Does not allow shadowing of variables.

Can add operator overloading to other types by implementing add and others. Then a + b becomes sugar for a.add(b). Theres quite a lot of names, and they could clash other uses.

Uses and and or rather than && and ||. They are short circuiting.

Like Austral, does not have infix operator precedence, parens are needed unless the same operator is used. I do sort of like this idea.

Has wrap around behavior for int arithmatic by default. Can use "unsafe" versions by suffixing with ~ so a +~ b is equivalent to a.add_unsafe(b). Makes division by zero return 0 in the "safe" style. Also has partial and checked arithmetic. Partial errors on overflow/underflow/div 0. Checked, returns the value and a bool if there is a problem. Uses ? suffix for partial.

Adds elseif to avoid the issue of knowing which if (with nested if) the else applies to.

In pony, control structures are expressions like hare. Returns the value of the last evaluated expression in general. Note that because of assignment behavior, if the last expression is an assignment it will return the last value of the thing being assigned to. for and while can have else clause to handle the case when there is no looping. break can return an expression (which will be the value the loop returns).

It's perhaps worth adding, that pony has None and so if there isn't a value from a loop (say it's not hit), the return value will be a union of the possible values.

There are no global functions, every function belongs to a type. There is no overloading.

Constructors are named, meaning there can be many constructors. They are prefixed with new.

Can specify arguments by name as well as positionally. This uses where keyword.

Methods can be chained using .>. object.>method() is roughly equivalent to `(object.method(); object). Chaining discards the return value.

Method names must start with lower case or _. If prefixed with _ the method is private.

Error handling is straight forward, but seems a little limited in that no data about the error seems to be passed back. Functions are defined to be able to return an error as partial functions. They are marked with ? and use try else at use site.

Equality uses == to determine if same value or is to check the same identity.

Uses with to mark block such that dispose is later called on a type.

Has partial application idea, that under the hood works like a lambda. Uses ~ as the partial application operator.

Uses consume to indicate something like a C++ move.

Has Object Capabilities which are quite similar to Austral linear capabilities.

Pony’s “whole program” approach to compilation means the compiler can work out as much as possible at compile time. source

Does this imply its a "closed world"? Being closed world does allow more efficient casting.

The pattern matching system via match is like a turbo charged switch. It can match on values, expressions, and types.

To cast as can be used, will produce an error if it fails and so needs to typically use try.

Types don't generarlly coerce, but there are methods to convert types, for example F32(1.0).f64().

Captures as used with if is matching by type, as well as doing a cast.

C-FFI, uses ifdef in order to control which FFI function is actually used.

Has a fairly extensive stdlib.

The package system seems straight forward. It uses a somewhat unusual scheme indicator, which indicates what the use is for.

Reread...

Links