Post

Using Enums

Enums (short for enumerations) are a way to define a type that can only take one of a few predefined values. They are useful for representing a set of related constants or options.

Declaring an Enum

In Glu, you can declare an enum using the enum keyword followed by the enum’s name, underlying type, and its cases.

Here is an example of an enum declaration:

1
2
3
4
5
6
enum Direction: Int8 {
    NORTH,
    SOUTH,
    EAST,
    WEST,
}

In this example, the enum Direction has an underlying type of Int8, which means the enum will be represented as an 8-bit integer. The cases NORTH, SOUTH, EAST, and WEST are the possible values that a variable of type Direction can take:

1
let direction: Direction = Direction::NORTH;

When referencing a case of an enum, you must use the double colon :: scope resolution operator, followed by the case name.

Explicit Raw Values

You can assign explicit raw values to the cases of an enum. This is useful when you want to assign specific values to each case.

Here is an example of an enum with explicit raw values:

1
2
3
4
5
enum Tile: Char {
    EMPTY = ' ',
    WALL = '#',
    PLAYER = '@',
}

In this example, the enum Tile has an underlying type of Char, and each case has an explicit raw value assigned to it. The raw value is specified after the case name using the assignment operator =.

The raw values must be unique within the enum and must match the underlying type of the enum. If you don’t specify raw values for the cases, the compiler will automatically assign them incrementing values starting from 0.

The raw values can be of any integer type, such as Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64, or Char.

Accessing Raw Values

The raw value of an enum case can be accessed using the rawValue property, and enum cases can be created from raw values using initializer syntax:

1
2
3
let tile: Tile = Tile::PLAYER;
let rawPlayer: Char = tile.rawValue;
let playerTile: Tile = { rawPlayer };

In this example, the raw value of the PLAYER case is accessed using the rawValue property, and a new Tile instance is created from the raw value using initializer syntax.

If you try to create an enum case from a raw value that doesn’t match any of the cases, the program will throw a runtime error. This makes it safer to work with enums, as you can be sure that the raw value corresponds to a valid case. If this behavior is not desired, you can instead use an open enum.

Open Enums

An open enum allows creating enum cases from raw values that don’t match any of the defined cases. This can be useful when working with external data sources or when you want to allow additional cases to be added in the future.

To declare an open enum, you can use the @open attribute before the enum keyword:

1
2
3
4
5
6
7
@open enum Color: UInt32 {
    RED = 0xFF0000,
    GREEN = 0x00FF00,
    BLUE = 0x0000FF,
}

let yellow: Color = { 0xFFFF00 };

In this example, the Color enum is declared as open, allowing the creation of additional cases from raw values that don’t match the defined cases. The yellow variable is assigned the color yellow, which is not explicitly defined in the enum.

When working with open enums, you should be aware that values of this type can be created from any value of the underlying type, not just the explicitly defined cases.

Overloading Operators

Just like any other type, enums can have operators overloaded to provide custom behavior. This can be useful when you want to define custom behavior for comparison operators or arithmetic operators.

Here is an example of overloading the bitwise OR operator | for the Color enum:

1
2
3
operator |(a: Color, b: Color) -> Color {
    return { a.rawValue | b.rawValue };
}

In this example, the | operator is overloaded to perform a bitwise OR operation on two Color instances. The operator calls the bitwise OR operator on the raw values of the two colors and returns a new Color instance with the result.

This allows you to use the | operator to combine two colors:

1
let magenta: Color = Color::RED | Color::BLUE;

In this example, the magenta variable is assigned the color magenta, which is a combination of red and blue.

Restricting Enums

Enums can use another enum as their underlying type, which permits creating an enum that can only take a subset of the values of the underlying enum. This can be useful when you want to restrict the possible values of an enum to a specific subset.

Here is an example of an enum that restricts the possible values to a subset of the Color enum:

1
2
3
4
5
enum PrimaryColor: Color {
    RED = Color::RED,
    GREEN = Color::GREEN,
    BLUE = Color::BLUE,
}

In this example, the PrimaryColor enum restricts the possible values to the primary colors red, green, and blue, which are defined in the Color enum. Because PrimaryColor is a closed enum (it is not defined with the @open attribute), it can only take the values explicitly defined in the enum: RED, GREEN, and BLUE.

Because PrimaryColor uses Color as its underlying type, its raw value will be of type Color:

1
2
3
let primaryColor: PrimaryColor = PrimaryColor::RED;
let color: Color = primaryColor.rawValue;
std::assert(color == Color::RED);

Because the raw value of PrimaryColor::RED is defined as Color::RED, the raw value of primaryColor will be Color::RED. This allows you to convert between the two enums and ensures that the values are compatible.

Conclusion

Enums are a powerful feature in Glu that allow you to define custom types with a fixed set of values. They can be used to represent a set of related constants or options and provide type safety and clarity to your code. By using enums, you can make your code more expressive and easier to understand.

This post is licensed under CC BY 4.0 by the author.