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
None
to 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 ofNone
. - 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
true
orfalse
. - 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
and _final
. _init
is called before any actor starts. _final
is called after all actors have terminated. The two functions take no parameter. The _init
and _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.