In Lily, an enum is a data type that is limited to a fixed set of potential options. Those options (termed variants) can require values, but they don't have to. An enum, like a class, can also include methods.

Enums have a plethora of potential. The predefined Option enum provides a Some that can contain a value, and a None, thereby providing a safe alternative to values that could be null.

Declaration

Suppose that you want to provide the user a choice between three different colors: Red, green, and blue.

enum Rgb
{
    Red,
    Green,
    Blue,
}

var color = Rgb.Red

The variants of an enum are accessible by using the enum's name. This is similar to how the methods of a class are accessible by using the class name. This is intentional, as it allows multiple enums in the same module to define Red without causing confusion. The only enums exempt from that rule are the predefined enums, Option and Result.

In the above example, color has the value Rgb.Red. However, the type of color is inferred to be Rgb.

Speaking of inference, writing Rgb.Red is a bit tedious. If the interpreter can infer an enum type, .variant can be written instead. This feature is termed variant shorthand, and it looks like this:

enum Rgb { Red, Green, Blue }

var color: Rgb = .Blue

There's a subtle difference in the declaration of Rgb this time: In the first example, there's a comma after the last variant. Here, there is none. The interpreter freely allows both styles.

Enums can also specify generics. The predefined Option enum, for example, looks like this:

enum Option[A] {
    Some(A),
    None
}

Using variants

Before exploring what features that enums as a whole have, it's important to be aware of how to use variants. These examples will use Option, which provides a Some and a None.

This section gives a brief overview of the match and with portions of the blocks tutorial. See that for more detail on what you can do with those.

Suppose you have an Option[Integer], and you'd like to know what's inside of it.

You can use match, which allows you to unpack values. It's also exhaustive, so the interpreter will prevent you from forgetting a case.

define is_integer(value: Option[Integer]): Boolean
{
    match value: {
        case Some(s):
            return true
        case None:
            return false
    }
}

print(is_integer(Some(1))) # true
print(is_integer(None)) # false

You can select for a particular variant using with. You can handle the others using an else, or ignore them. This could use the value in the Some, if it checked that instead of the None.

define is_integer(value: Option[Integer]): Boolean
{
    with value as None: {
        return false
    else:
        return true
    }
}

print(is_integer(Some(1))) # true
print(is_integer(None)) # false

When using match or with against a value, the interpreter allows only variants belonging to the enum. As a result, even if Rgb was used in the above examples, you'd use just case Blue or as Red and not case Rgb.Blue or as Rgb.Red.

If the above are not suitable for the task, it is possible to compare variants directly. When variants are compared with each other, they are considered equal if they are structurally equal. Thus, Some(1) == Some(1) is true.

define is_integer(value: Option[Integer]): Boolean
{
    if value == None: {
        return false
    else:
        return true
    }
}

print(is_integer(Some(1))) # true
print(is_integer(None)) # false

The above examples print out the function results, but what about the variants themselves? When variants are printed or interpolated, they're rendered as they're written:

print(Some(1)) # Some(1)
print(None) # None

enum Speed { Slow, Medium, Fast }

var s: Speed = .Fast

print(s) # Speed.Fast

In the example where match is used, the value inside the Some is unpacked into the variable s. If that function was to assign a new value to s, it would not mutate the underlying value:

define modify_some(a: Option[Integer])
{
    with a as Some(s): {
        s = s + 100
    }
}

var v = Some(10)

modify_some(v)
print(v) # Some(10)

The initialization of v to Some(10) may make it seem as though Some is a function. Variants are allowed to have a value piped to them, so 10 |> Some is equivalent to Some(10). When a variant is referenced, it must be created in whole.

Methods

Enums, like classes, allow methods to be defined within them. Unlike classes, enum methods do not allow for qualifiers: No static and no forward. Since they cannot be inherited from, they are all public, but without that needing to be explicitly stated. Simply use define to create a new method.

enum Rgb
{
    Red,
    Green,
    Blue,

    define is_blue: Boolean
    {
        match self: {
            case Blue:
                return true
            case Red, Green:
                return false
        }
    }
}

var v: Rgb = .Blue

print(v.is_blue())    # true

# An alternative way of calling the above.
print(Rgb.is_blue(v)) # true

# Enum methods are available on variants.
print(Some(1).is_none()) # false

All enum methods are non-static, and thus have a self that can be operated upon. The match of the is_blue method is guaranteed to be exhaustive because enums do not allow more variants after methods are defined.

Arguments

It is possible for a variant to take arguments. Those arguments can include the enum itself. Variants that take values can be called like a Function, but they are not Function values.

Variants support all argument types, except optional arguments.

enum Tree
{
    Leaf(Integer),
    Branch(Tree, Tree),

    define walk: Integer
    {
        match self: {
            case Leaf(value):
                return value
            case Branch(left, right):
                return left.walk() + right.walk()
        }
    }
}

var tree =
    Tree.Branch(
        .Branch(
            .Leaf(10),
            .Leaf(20)
        ),
        .Leaf(30)
    )

print(tree.walk()) # 60

Variants can also have variable arguments.

enum Tree
{
    Leaf(Integer),
    Branch(Tree...),

    define walk: Integer
    {
        match self: {
            case Leaf(value):
                return value
            case Branch(targets):
                var total = 0

                for t in targets: {
                    total += t.walk()
                }

                return total
        }
    }
}

var tree =
    Tree.Branch(
        .Branch(
            .Leaf(10)
        ),
        .Leaf(20),
        .Leaf(30),
        .Leaf(40)
    )

print(tree.walk()) # 100

Keyword arguments are also supported.

enum Color
{
    Blue,
    Green,
    Red,
    RGB(:red Integer, :green Integer, :blue Integer),

    define to_i: Integer
    {
        match self: {
            case Blue:
                return 0x0000ff
            case Green:
                return 0x00ff00
            case Red:
                return 0xff0000
            case RGB(r, g, b):
                return (r << 16) +
                       (g << 8) +
                       b
        }
    }
}

var v: Color = .RGB(:red 0xff,
                    :blue 0,
                    :green 0)

print(Color.Green.to_i()) # 65280
print(v.to_i())           # 16711680

Variants support call piping.

{
    var v = Option.unwrap(Some(1))

    print(v) # 1
}
{
    var v = 1 |> Some |> Option.unwrap

    print(v) # 1
}

Value enums

In the above examples, variants do not themselves have any underlying value. The None of an Option is a None. Additionally, while enums be inherited from, they can inherit.

Enums are permitted to inherit from one (and only one) class: Integer.

If an enum is specified as inheriting from Integer, it is created as a value enum. Similarly, the variants are called value variants.

enum Color < Integer {
    Red,        # No value given, defaults to 0
    Green = 12,
    Blue,       # 13 (the prior value + 1)

    define is_green: Boolean
    {
        match self: {
            case Green:
                return true
            else:
                return false
        }
    }
}

print(Color.Red) # 0
print(Color.Green) # 12
print(Color.Blue) # 13

print(Color.Blue.is_green()) # false

# Math is allowed, but the result decays to Integer.
var some_int: Integer = Color.Green + Color.Blue

print(some_int) # 25

If the first value isn't given, it defaults to 0. All others default to the prior value + 1. Value variants must be initialized with a literal value, and they must be unique to each other. Each variant must be empty as well.

Internally, value enums are implemented as Integer values, instead of as a separate class.

import introspect

enum Color < Integer {
    Red,
    Green,
    Blue
}

print(Color.Red |> introspect.class_name) # Integer

Because value enums inherit from Integer, they are allowed to use Integer methods.

enum Color < Integer {
    Red,
    Green,
    Blue
}

print(Color.Green.to_hex()) # 0x1