Skip to content

Primitives

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.
// 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 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.