A primitive is similar to a class, but there are two critical differences:
- A primitive has no fields.
- There is only one instance of a user-defined primitive.
Having no fields means primitives are never mutable. Having a single instance means that if your code calls a constructor on a primitive type, it always gets the same result back (except for built-in "machine word" primitives, covered below).
What can you use a primitive for?
There are three main uses of primitives (four, if you count built-in "machine word" primitives).
- As a "marker value". For example, Pony often uses the primitive
Noneto indicate that something has "no value". Of course, it does have a value, so that you can check what it is, and the value is the single instance of
- As an "enumeration" type. By having a union of primitive types, you can have a type-safe enumeration. We'll cover union types later.
- As a "collection of functions". Since primitives can have functions, you can group functions together in a primitive type. You can see this in the standard library, where path handling functions are grouped in the primitive
Path, for example.
// 2 "marker values" primitive OpenedDoor primitive ClosedDoor // An "enumeration" type type DoorState is (OpenedDoor | ClosedDoor) // A collection of functions primitive BasicMath fun add(a: U64, b: U64): U64 => a + b fun multiply(a: U64, b: U64): U64 => a * b actor Main new create(env: Env) => let doorState : DoorState = ClosedDoor let isDoorOpen : Bool = match doorState | OpenedDoor => true | ClosedDoor => false end env.out.print("Is door open? " + isDoorOpen.string()) env.out.print("2 + 3 = " + BasicMath.add(2,3).string())
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.
Built-in primitive types
The primitive keyword is also used to introduce certain built-in "machine word" types. Other than having a value associated with them, these work like user-defined primitives. These are:
- Bool. This is a 1-bit value that is either
- ISize, ILong, I8, I16, I32, I64, I128. Signed integers of various widths.
- USize, ULong, U8, U16, U32, U64, U128. Unsigned integers of various widths.
- F32, F64. Floating point numbers of various widths.
ISize/USize correspond to the bit width of the native type
size_t, which varies by platform. ILong/ULong similarly correspond to the bit width of the native type
long, which also varies by platform. The bit width of a native
int is the same across all the platforms that Pony supports, and you can use I32/U32 for this.
Primitive initialisation and finalisation
Primitives can have two special functions,
_init is called before any actor starts.
_final is called after all actors have terminated. The two functions take no parameter. The
_final functions for different primitives always run sequentially.
A common use case for this is initialising and cleaning up C libraries without risking untimely use by an actor.