Skip to main content

Values & Data

Raven uses bundle to describe new data types, similar to ADTs in functional languages.

Bundles

The simplest bundle is a named collection of fields:

bundle Tape(data: List, i: Int64)

tape = Tape([0], 1)

Field annotations are optional, just like in functions. A bundle can also have several variants:

bundle Maybe { Some(x), Nil() }

show Some(5) # Some(5) = Some(5)

Each variant is a constructor. A value of type Maybe is either a Some(x) or a Nil(), and you can match on which (see below).

Fields can splat, giving a bundle with a variable number of parts – this is how the standard library defines lists:

bundle List(xs...)

Getting data back out

Fields are accessed by pattern matching – there's no .field syntax on bundles. Unpack in an assignment, a match, or directly in a function signature:

bundle Complex(re, im)

fn abs2(Complex(re, im)) {
re^2 + im^2
}

Custom constructors and methods

A constructor is just a function, so you can overload it like any other. Here's a default constructor and a custom indexing method for Tape:

fn Tape() { Tape([0], 1) }

fn (tape: Tape(xs, i))[] { xs[i] }

The fn (a: T) op (b: T) form defines operators, and fn (xs: T)[i] defines indexing:

fn (a: Complex) + (b: Complex) {
Complex(real(a) + real(b), imag(a) + imag(b))
}

Value semantics

Bundles – like lists and everything else in Raven – are values. Two values with the same contents are equal, and "modifying" one variable never affects another:

xs = [1, 2, 3]
ys = xs
append(&ys, 4)
show xs # xs = [1, 2, 3]
show ys # ys = [1, 2, 3, 4]

This is worth dwelling on, because it's one of Raven's most important design choices. In most high-level languages, compound data is a reference: passing a list around means passing a pointer to shared, mutable state, and a change made in one corner of the program can be observed in another. In Raven, data behaves like numbers do everywhere: x = y gives x its own copy, conceptually, and nothing you do to y afterwards can touch it.

That might sound expensive, but it isn't: the compiler uses reference counting to share the underlying storage, copying only when a value is actually modified while someone else still holds it. When a value is used uniquely – which is most of the time – updates happen in place, just as fast as mutation.

What you get in exchange:

  • No spooky action at a distance. The only way a function changes your variable is if you passed it with &.
  • No cycles. Values can't refer to themselves, so append(&xs, xs) just puts a copy of the old xs inside the new one.
  • Cheap reasoning. Equality is structural, and you never need to think about identity vs equality, defensive copies, or freezing.

Records

For quick key-to-value data, the standard library has record – ordered pairs keyed by tags:

d = record()
setkey(&d, tag"name", "Raven")
getkey(d, tag"name") # "Raven"

Records support getkey, setkey, haskey and merge. They're a small, linear-scan structure – proper hash maps are on the roadmap.