An owning reference keeps an object alive. When a struct has no owning reference pointing to it anymore, the struct is deallocated.
Every mutable struct has exactly one owning reference pointing to it. 0
import stdlib.*;
struct Spaceship {
name str;
fuel int;
}
exported func main() {
ship = Spaceship("Serenity", 2);
// ship is an owning reference
println(ship.name);
// implicit (ship).drop()
}
Immutables, such as primitives or immutable structs, can have multiple owning references pointing to them. 1
When we say a = b; for an immutable struct, both a and b can still be used.
Immutables are simple to use. The rest of the page is about references to mutable structs.
import stdlib.*;
struct Vec3 imm {
x int;
y int;
z int;
}
exported func main() {
firstRef = Vec3(3, 4, 5);
otherRef = &firstRef;
// Can use both freely.
println(firstRef.x + otherRef.y);
}
7
A local can give up the owning reference. Afterwards, that local is gone, and we cannot use it. This is called a move.
This is used to influence when the struct is dropped, to keep it alive for longer or destroy it sooner.
In this example, the a local is giving up the owning reference, and we're putting it into the b local.
struct Spaceship {}
exported func main() {
a = Spaceship(); 2
b = a; // Move the ship from a to b.
// b now owns the Spaceship.
// Can't use a now.
}
It works the same way when passing an owning reference to a function.
import stdlib.*;
struct Spaceship { name str; }
func foo(b Spaceship) {
println(b.name);
}
exported func main() {
a = Spaceship("Raza");
foo(a); // Move a into foo's b
// Can't use a now.
}
Ownership is also found in C++ (unique_ptr), Rust, and Cyclone.
C also has "conceptual" ownership, in that we must track ownership without the language's help, to know when to free an object.
Vale's references has the flexibility of C++'s unique_ptr while guaranteeing memory safety.
These are references from the user's perspective, but under the hood, references to small immutables (including primitives and small structs) are inlined and copied around, for performance.
Rather than thinking "a is an owning reference", think instead that a is a local that currently contains an owning reference.
On the next line, a is destroyed, but the owning reference that it contained still lives on (inside b).
Vale has two kinds of non-owning references:
These are explained in more detail below.
Note that there are no shared-ownership references 3 in Vale, except for immutable structs.
Like C++'s shared_ptr or Rust's Rc or all references in Java and Python.
To make a borrow reference, use the & symbol.
In this example, we're making a borrow reference to ship and giving it to the foo function.
import stdlib.*;
struct Spaceship {
name str;
numWings int;
}
func foo(s &Spaceship) {
println(s.name);
}
exported func main() {
ship = Spaceship("Serenity", 2);
foo(&ship);
}
We often have a reference to a struct, and we would be fine with that struct being deallocated while we have a reference to it.
In these situations, we use weak references.
A weak reference will set itself to null when the object it's pointing at is destroyed.
We can make a weak reference with &&.
To use a weak reference, call lock, which will return an optional borrow reference (?&Spaceship). We can then check if it contains a &*Spaceship or is empty.
In other words, we cannot reach into a struct through a weak reference directly; we must lock it to try to make it into a borrow reference first.
In this example, we're making a o borrow reference from the ship.origin weak reference.
import stdlib.*;
weakable struct Base {
name str;
}
struct Spaceship {
name str;
origin &&Base;
}
func printShipBase(ship &Spaceship) {
maybeOrigin = lock(ship.origin); 45
if (not maybeOrigin.isEmpty()) { 6
o = maybeOrigin.get();
println("Ship base: " + o.name);
} else {
println("Ship base unknown!");
}
}
exported func main() {
base = Base("Zion");
ship = Spaceship("Neb", &&base);
printShipBase(&ship);
(base).drop(); // Destroys base.
printShipBase(&ship);
}
Ship base: Zion
Ship base unknown!
Locking a weak reference gives us a ?&Spaceship, also known as an Opt<&Spaceship>. This has two possible subclasses: None<&Spaceship> which is empty and Some<&Spaceship> which contains a &Spaceship.
In future versions, these three lines can be reduced to if ((o) = lock(ship.origin)) {.
This would specifically check if that ?&Spaceship is the one-member subclass (Some<&Spaceship>) and if so, assigns its member to o. This is a conditional destructure, see Pattern Matching for more.
In future versions, we could also use a match (match statement) here to check whether the ?&Spaceship contained anything.
These are planned features for Vale. See the roadmap for plans!
Inlining is used for optimization.
In these early days of Vale, objects are always on the heap. Soon, objects will by default be "inline", which means contained on the stack or inside their containing struct. Objects can be put on the heap with the ^ keyword.
This example puts the Spaceship on the heap.
import stdlib.*;
struct Spaceship {
name str;
numWings int;
}
exported func main() {
ship = ^Spaceship("Enterprise", 4);
println(ship.name);
}
Enterprise
Next: Interfaces