Skip to content

Used defined data types

Sisypheus edited this page Apr 15, 2024 · 2 revisions

Wonder why l[0] is not an integer but an optional integer? And also wonder what is an optional integer. Let's dive into the user-defined data types in Plume.

Compound types

Compound types are types that are composed of other types and are used to represent more complex data structures. In Plume, you can define compound types using the type keyword followed by the name of the type and its definition.

// Define a compound type named `point` that represents a point in a 2D space
type point { point(int, int) }

In the previous example, we have defined a compound type named point that represents a point in a 2D space. The type is composed of two integers that represent the x and y coordinates of the point.

But data types can be also single values. You could for instance define a type unit that represents a single value.

// Define a compound type named `unit` that represents a single value
type unit { unit }

That's a bit weird, isn't it? But it's a common pattern in functional programming languages to have a type that represents a single value. That's how it's defined in the standard library in particular.

Generic compound types

Compound types can also be generic, meaning that they can accept any type of value. You can define a generic compound type by writing the type name followed by a list of generic types in angle brackets.

// Define a generic compound type named `pair` that represents a pair of values
type pair<A, B> { pair(A, B) }

And we could use this type to represent a pair of integers.

// Define a pair of integers
my_pair: pair(int, str) = pair(42, "hello world")

To infinity constructors, and beyond!

That's where the serious stuff begins. In Plume, you may want to define several constructors for a single type. That is what we call a sum type.

For instance, we could define a linked-list implementation using a sum type.

// Define a sum type named `list` that represents a linked list
type list<A> {
  nil,
  cons(A, list<A>)
}

In the previous example, we have defined a sum type named list that represents a linked list. The type is composed of two constructors: nil which represents an empty list and cons which represents a list with a head element and a tail list.

We could use this type to represent a list of integers.

// Define a list of integers
my_list: list<int> = cons(1, cons(2, cons(3, nil)))

Pattern matching

Using all these features, we want now to interact with the data types we have defined, and that is where pattern matching comes into play. Pattern matching is also used to match a value against user-defined types:

// Define a function that computes the length of a list using pattern matching
fn length(l: list<A>): int => switch l {
  case nil => 0
  case cons(?, xs) => 1 + length(xs)
}

This is easy to understand: if the list is empty, then the length is 0. Otherwise, the length is 1 plus the length of the tail of the list. Computing the length of a list is like summing 1 for each element of the list.