Structs and enums
Structs and enums are Luce's value types: like numbers, they are copied when assigned or passed. Classes, the other kind of user type, are shared; Classes and memory covers them and when to choose which.
Structs#
struct Rectangle:
let width: float
let height: float
var label: str = "untitled"
func area(self) -> float:
return self.width * self.height
pub func main(arguments: list[str]) -> int!:
let a = Rectangle(width = 2.0, height = 3.0)
let b = Rectangle(2.0, 3.0, "door")
print(a, a.area())
print(b.label, a == b, a == Rectangle(2.0, 3.0))
return 0Rectangle(width = 2.0, height = 3.0, label = untitled) 6.0 door false true
- Fields are
letorvar, with a type and an optional default. A default is a constant or an empty collection, made afresh for each struct built. - Construction gives the fields by position, in declaration order, or by name; fields with defaults may be left out. This is what Python's
@dataclassgenerates. A struct can replace it with its owninit(below). - Equality, hashing and printing are worked out from the fields: two structs are equal when all their fields are, and a struct prints as
Name(field = value, ...). They exist when every field supports them; a struct with a class field, for instance, has no==unless that class declaresEquatable. - Private fields (those not marked
pub) are left out of construction in other modules, and there a struct that has one prints only through aDisplayof its own (Visibility). - A struct can declare interfaces,
struct Circle: Shape:, and replace the generated equality, ordering or display by declaringEquatable,OrderedorDisplay(Interfaces).
Its own init#
A struct can declare init when construction should do more than store the fields: convert units, compute a field, or check the values. Name(...) then calls it, and the memberwise form is no longer available:
let below_absolute_zero = ErrorCode.package(1)
struct Celsius:
let degrees: float
var label: str = "°C"
func init(self, fahrenheit: float) -> unit!:
if fahrenheit < -459.67:
error(below_absolute_zero, "below absolute zero")
self.degrees = (fahrenheit - 32.0) * 5.0 / 9.0
pub func main(arguments: list[str]) -> int!:
print(Celsius(212.0))
let cold = Celsius(-500.0) catch failure:
print(failure.message)
recover Celsius(32.0)
print(cold.degrees, cold.label)
return 0Celsius(degrees = 100.0, label = °C) below absolute zero 0.0 °C
Fields with defaults get them before init runs, and init must assign every other field. An init that can fail is declared -> unit!, as for classes (Classes).
A struct cannot contain itself directly, var next: Node? inside struct Node, since a value would then have no fixed size. A list of itself is allowed, var children: list[Tree], and so is a class that refers to itself.
Values copy#
Assigning a struct, passing it, returning it or putting it in a collection copies it:
struct Point:
var x: int
var y: int
func moved_right(point: Point) -> Point:
var copy = point
copy.x += 1
return copy
pub func main(arguments: list[str]) -> int!:
let a = Point(x = 0, y = 0)
var b = a
b.x = 5
let c = moved_right(b)
let points = [a, b]
b.y = 9
print(a, b, c, points)
return 0Point(x = 0, y = 0) Point(x = 5, y = 9) Point(x = 6, y = 0) [Point(x = 0, y = 0), Point(x = 5, y = 0)]
In Python, b = a makes b another name for the same object, and b.x = 5 would change a too. Here each name has its own point. The same goes for points: the list holds copies made when it was built, so the later b.y = 9 does not reach it.
The copy is shallow: a struct that holds a list or a class object holds a reference to it, and the copies share that object.
struct Inventory:
let owner: str
var items: list[str] = []
pub func main(arguments: list[str]) -> int!:
let first = Inventory(owner = "Ada")
var second = first
second.items.append("lamp")
print(first.items, second.items)
return 0[lamp] [lamp]
A let struct cannot change at all: none of its fields can be assigned, var fields included. To change a struct, bind it with var.
Methods that change self#
A method that assigns to a field of self changes the value it is called on. The compiler recognises such methods by themselves, and allows calling them only on something that can change: a var, or a var field of a var.
struct Counter:
var count: int = 0
func increment(self):
self.count += 1
pub func main(arguments: list[str]) -> int!:
var counter = Counter()
counter.increment()
counter.increment()
print(counter.count)
return 02
With let counter = Counter(), the call counter.increment() is an error: "the method increment changes its receiver, which must be a var". Swift writes such methods as mutating; Luce infers it.
Enums#
An enum is a closed set of cases, each with optional data:
enum Token:
number(value: int)
word(text: str)
end
func describe(self) -> str:
return match self:
.number(value) => f"the number {value}"
.word(text) => f"the word {text}"
.end => "the end"
pub func main(arguments: list[str]) -> int!:
let tokens = [Token.number(3), .word(text = "hi"), .end]
for token in tokens:
print(token, token.describe())
print(Token.word("hi") == Token.word(text = "hi"), {Token.end: 1})
return 0Token.number(value = 3) the number 3
Token.word(text = hi) the word hi
Token.end the end
true {Token.end: 1}- A case's data is declared like parameters, and given by position or by name.
Token.number(3)names the type;.number(3)is enough where the type is known: an argument, a list of tokens, amatcharm, a comparison.- Enums are values: copied, compared, hashed and printed through their data, like structs.
- An enum can have methods and declare interfaces.
- The only way to read a case's data is
match, which binds it. There is no.valueattribute and noisinstancetest.
An enum replaces several Python idioms at once: an Enum of constants (cases without data), a Union of dataclasses (cases with data), and a string that should only take a few values. A case is never none: none means an empty optional, so it cannot name a case.
Python's Enum values can be numbers or strings. Luce's cannot: a case is just itself. Enums that must match numbers, for a file format or a C library, are declared in Base (Luce and Base).
The standard enum Result#
Result[T], with the cases .success(value: T) and .failure(reason: Error), is an ordinary enum available everywhere. It stores the outcome of an operation that can fail; Optionals and errors shows how to use it.
Generic structs and enums#
Structs and enums can take type parameters:
struct Pair[A, B]:
let first: A
let second: B
enum Tree[T]:
leaf(value: T)
node(left: list[Tree[T]], right: list[Tree[T]])
pub func main(arguments: list[str]) -> int!:
let pair = Pair(first = 1, second = "one")
let tree = Tree.node(left = [Tree.leaf(1)], right = [Tree.leaf(2)])
print(pair, tree)
return 0Pair(first = 1, second = one) Tree.node(left = [Tree.leaf(value = 1)], right = [Tree.leaf(value = 2)])
The type arguments are inferred from the values, or written: Pair[int, str]. Interfaces and generics has the rules.