A primitive is similar to a class, but there are two critical differences:

  1. A primitive has no fields.
  2. 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).

  1. 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 of None.
  2. As an “enumeration” type. By having a union of primitive types, you can have a type-safe enumeration. We’ll cover union types later.
  3. 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.
  1. // 2 "marker values"
  2. primitive OpenedDoor
  3. primitive ClosedDoor
  4. // An "enumeration" type
  5. type DoorState is (OpenedDoor | ClosedDoor)
  6. // A collection of functions
  7. primitive BasicMath
  8. fun add(a: U64, b: U64): U64 =>
  9. a + b
  10. fun multiply(a: U64, b: U64): U64 =>
  11. a * b
  12. actor Main
  13. new create(env: Env) =>
  14. let doorState : DoorState = ClosedDoor
  15. let isDoorOpen : Bool = match doorState
  16. | OpenedDoor => true
  17. | ClosedDoor => false
  18. end
  19. env.out.print("Is door open? " + isDoorOpen.string())
  20. 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 or false.
  • 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.