User defined types
Named types can be defined at the top level, using one of four keywords: data
, class
, capability
or newtype
.
data
To define an immutable type, you can use the data
keyword.
data Shape {
Circle(x: Float, y: Float, radius: Float)
Rectangle(x: Float, y: Float, width: Float, height: Float)
}
This defines a type called Shape
with two variants Circle
and Rectangle
.
The Circle
variant has three named fields of type Float
, while the Rectangle
variant has four.
Type and variant names must start with a capital letter.
No class
or capability
types can occur in the definition of a data
type.
A value of type Shape
is either a Circle
or a Rectangle
, and they can be constructed as follows:
Circle(0.0, 0.0, 1.0) // Variant, : Shape
Rectangle(5.0, 7.0, 3.0, 2.0) // Variant, : Shape
Above the variants are constructed using positional arguments, whose order must coincide with the order of the parameters in the type definition.
Named arguments are supported as well:
Circle(x = 0.0, y = 0.0, radius = 1.0)
Named parameters don't have to be in order, and can be mixed with positional arguments:
Circle(radius = 1.0, 0.0, 0.0)
To branch on the specific variant of a type, use pattern matching .
Common fields
When all the variants share a set of fields, they can be moved to the common fields section of the declaration:
data Shape(x: Float, y: Float) {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
}
This is similar to the Shape
definition above, but the x
and y
fields have been pulled out as common fields.
Given a value of a type, e.g. shape: Shape
, common fields can be accessed without knowing which specific variant it is:
shape.x // Returns a Float
shape.y // Returns a Float
When there is only one variant of a type, and its name coincides with the name of the type, we can use a shorthand definition:
data Point(x: Float, y: Float)
This defines a type called Point
with a single variant, also named Point
, and two common fields of type Float
.
Copying
If you have a value and want to construct a variant, you can copy each field explicitly:
Rectangle(x = point.x, y = point.y, width = 2.0, height = 1.5)
There's a shorthand for doing this, however:
point.Rectangle(width = 2.0, height = 1.5)
Using this shorthand, the fields of Rectangle
that aren't specified will be copied from point
.
class
Types defined with the class
keyword work like data
types, except for the differences noted here.
Fields of class
types may be declared mutable
:
class FruitBasket(
mutable apples: Int
mutable oranges: Int
mutable bananas: Int
)
Mutable fields can be updated. Given a value basket: FruitBasket
, its fields can be assigned to:
basket.apples = 42 // The apples field now holds the value 42
basket.oranges += 1 // The oranges field is now one greater
basket.bananas -= 1 // The bananas field is now one less
Except for capability
types, all types can occur in the definition of a class
type.
capability
Types defined with the capability
keyword work like class
types, except for the differences noted here.
capability EventHandler(
onEvent: () => Unit
)
The onEvent
field here contains a first class function, which may have captured other capabilities or classes in its closure.
Therefore, calling the function contained in this field may cause side effects.
In particular, it may have captured the system
argument that's passed to the main function, or other capabilities that allow it to do I/O.
There are no restrictions on the types of fields that capability
types can have.
Function types =>
are considered capability
types.
newtype
Types defined with the newtype
keyword work like data
types, except that they must have exactly one common field and no explicitly listed variants.
newtype UserId(id: Int)
At runtime, they are represented as values of the field type.
In this case, it means that UserId(42)
is represented as the Int
value 42
at runtime, with zero overhead.
Generic types
Whether you use data
, class
, capability
or newtype
to define a type, it may have type parameters.
data Basket[T](
items: List[T]
)
Here the T
in Basket[T]
is a type parameter, and it's used as a type argument in List[T]
.
An unbounded type parameter can be instantiated to any type.
We can have a Basket[Shape]
, which has a field items: List[Shape]
, and a different type Basket[EventHandler]
, which has a field items: List[EventHandler]
.
Note that type parameters are not concrete types, and are thus not subject to the field type restrictions stated earlier.
Anonymous records
An anonymous record is not defined anywhere, but consists of zero or more fields. The fields may have any type, but can't be reassigned after creation.
(red = 255, green = 255, blue = 0)
This constructs an anonymous record.
If you have an anonymous record value, e.g. color: (red: Int, green: Int, blue: Int)
, you can access its fields:
color.red // Returns an Int
color.green // Returns an Int
color.blue // Returns an Int