collectionsLuwuexperimental

collections provides the data structures you'd otherwise build out of tables: bundles of two to four values, an ordered map that remembers insertion order, growable arrays, double-ended queues, and options and results for values that can be missing or can fail. They're builtin classes implemented in C++ that the type checker fully understands.

option and result are tables that hold their variant classes. The types have the same names and come from the library the way a required module's types do:

Input
const .{ vec } = collections
type .{ vec } = collections

const scores: vec<number> = vec({ 10, 20 })
scores:push(30)
print(#scores)
Output
3

The library is a prototype, and its API can still change.

Summary

Bundles

collections.pairLuwuexperimental
Two values kept together
collections.tripleLuwuexperimental
Three values kept together
collections.quadLuwuexperimental
Four values kept together

Containers

collections.orderedmapLuwuexperimental
A map whose entries stay in insertion order
collections.vecLuwuexperimental
A growable array that holds one type of value
collections.vecdequeLuwuexperimental
A double-ended queue with constant-time adds and takes at either end

Options and results

collections.optionLuwuexperimental
A value that may be missing
collections.resultLuwuexperimental
The outcome of something that can fail: a value or an error

Bundles

collections.pairLuwuexperimental

class pair<A, B>(public const first: A, public const second: B)

Two values kept together, like a function's two results stored in one place. The fields are const: build a new pair to change them.

Input
const .{ pair } = collections

const point = pair("x", 4)
const .(axis, offset) = point
print(`{axis} = {offset}`)
print(point == pair("x", 4))
print(tostring(point))
Output
x = 4
true
pair("x", 4)

collections.tripleLuwuexperimental

class triple<A, B, C>(public const first: A, public const second: B, public const third: C)

Three values kept together. It works like pair with a third field.

collections.quadLuwuexperimental

class quad<A, B, C, D>(
    public const first: A,
    public const second: B,
    public const third: C,
    public const fourth: D
)

Four values kept together. It works like pair with a third and fourth field.

Containers

collections.orderedmapLuwuexperimental

class orderedmap<K, V>()
    function get(self, key: K): V?
    function set(self, key: K, value: V)
    function has(self, key: K): boolean
    function remove(self, key: K): V?
    function insert(self, position: number, key: K, value: V)
    function move(self, key: K, position: number)
    function movebefore(self, key: K, other: K)
    function moveafter(self, key: K, other: K)
    function swap(self, a: K, b: K)
    function sort(self, comesbefore: (K, K) -> boolean)
    function index(self, key: K): number?
    function at(self, position: number): (K?, V?)
end

A map whose entries stay in insertion order. A table's pairs visits keys in no particular order, and an orderedmap visits them in order for a menu, a leaderboard or the columns of a table. Finding the position of a key or the entry at a position takes constant time.

A position is a whole number from 1 to the number of entries (one more for insert). Any other position passed to insert or a move raises an error.

Input
const .{ orderedmap } = collections

const stock = orderedmap()
stock:set("pears", 4)
stock:set("apples", 12)
stock:insert(1, "kiwis", 7)
stock:moveafter("kiwis", "apples")

for fruit, count in stock do
    print(`{fruit}: {count}`)
end
print(stock:index("kiwis"))
Output
pears: 4
apples: 12
kiwis: 7
3

collections.vecLuwuexperimental

class vec<T>(items: { T }?)
    function push(self, value: T)
    function pop(self): T?
    function at(self, position: number): T?
    function set(self, position: number, value: T)
    function insert(self, position: number, value: T)
    function remove(self, position: number): T
    function clear(self)
end

A growable array that holds one type of value. The first value it gets fixes the type: a vec of numbers raises an error when you push a string, and a vec of objects takes objects of the same class only. nil never goes in. The optional array passed to the constructor fills the vec in order.

set, insert and remove raise an error for a position out of range.

Input
const .{ vec } = collections

const names = vec({ "Ada", "Grace" })
names:push("Linus")
names:insert(1, "Alan")
print(names:remove(3))
print(tostring(names))

const ok = pcall(function()
    names:push(42)
    return true
end)
print(ok)
Output
Grace
vec({"Alan", "Ada", "Linus"})
false

collections.vecdequeLuwuexperimental

class vecdeque<T>(items: { T }?)
    function pushfront(self, value: T)
    function pushback(self, value: T)
    function popfront(self): T?
    function popback(self): T?
    function front(self): T?
    function back(self): T?
    function at(self, position: number): T?
    function set(self, position: number, value: T)
    function clear(self)
end

A double-ended queue: adding and taking values at either end takes constant time. Taking the first value of an array with table.remove(t, 1) moves every other value down. A vecdeque takes it without moving the rest. Like vec, it holds one type of value fixed by the first one it gets.

Input
const .{ vecdeque } = collections

const jobs = vecdeque({ "build", "test" })
jobs:pushback("deploy")
jobs:pushfront("fetch")
while #jobs > 0 do
    print(jobs:popfront())
end
Output
fetch
build
test
deploy

Options and results

collections.optionLuwuexperimental

export type option<T> = some<T> | none

class some<T>(public const value: T)
    function get(self): T
    function expect(self, message: string?): T
end

declare collections: {
    option: { some: class<some>, none: none },
}

A value that may be missing. A full option is a some object that holds the value, and an empty option is none itself. option.none is the same none and allocates nothing.

none has get and expect only while the library is loaded. Indexing none with any other key is an error.

Input
const .{ option } = collections
type .{ option } = collections

const function find_user(id: number): option<string>
    return if id == 1 then option.some("Ada") else option.none
end

print(find_user(1):get())
print(find_user(2):get() == none)
print(tostring(find_user(1)))
Output
Ada
true
option.some("Ada")

collections.resultLuwuexperimental

export type result<T, E> = ok<T> | err<E>

class ok<T>(public const value: T)
    function ok(self): T
    function err(self): none
    function expect(self, message: string?): T
end

class err<E>(
    public const error: E,
    public const module: string?,
    public const line: number?,
    public const traceback: string?
)
    function ok(self): none
    function err(self): E
    function expect(self, message: string?): never
end

declare collections: {
    result: { ok: class<ok>, err: class<err> },
}

The outcome of something that can fail: an ok object with a value or an err object with an error. A function that returns a result makes its caller handle the failure instead of catching an error it raised.

A try expression runs an expression and gives a result: ok with its value, or err with the error it raised.

Input
const .{ result } = collections
type .{ result } = collections

const function parse_port(text: string): result<number, string>
    const port = tonumber(text)
    if port and port >= 1 and port <= 65535 then
        return result.ok(port)
    end
    return result.err(`'{text}' is not a port`)
end

print(parse_port("8080"):ok())
print(parse_port("http"):err())
print(tostring(try tonumber("12")))
Output
8080
'http' is not a port
result.ok(12)