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