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