Here is a basic Spaceship struct, with a couple members.
We can construct it using its constructor function, which has the same name and was automatically generated.
All structs are mutable by default, more on that in the Mutability section below.
import stdlib.*;
struct Spaceship {
name str;
numWings int;
}
exported func main() {
ship = Spaceship("Serenity", 2);
println(ship.name);
}
Serenity
Recall how we can use set x = 10; to modify a pre-existing variable x to 10.
We can use the set keyword with struct members too.
Here, we change the name member to "Raza".
struct Spaceship {
name str;
}
exported func main() {
ship = Spaceship("Serenity");
set ship.name = "Raza";
}
We can specify a custom constructor for our struct.
We just need to give it the same name as the struct. 0
import stdlib.*;
struct Spaceship {
name str;
numWings int;
}
func Spaceship() Spaceship {
Spaceship("Serenity", 2)
}
exported func main() {
ship = Spaceship();
println(ship.name);
}
Inside the constructor, we must call either another constructor or constructor<T>.
Every mutable struct has exactly one owning reference at any given time. 1 2
When we create a mutable struct, we get the owning reference to it. When the owning reference disappears, the struct is automatically deallocated (via drop, explained below).
import stdlib.*;
struct Spaceship { name str; fuel int; }
exported func main() {
ship = Spaceship("Serenity", 2);
// ship is an owning reference
println(ship.name);
// implicitly drops ship
}
There are other kinds of references (constraint, borrow, weak), References explains more.
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 a struct.
Vale's ownership has the flexibility of C++'s unique_ptr without the mutability and aliasing restrictions of Rust and Cyclone, see References to learn how.
Every mutable struct has exactly one owning reference pointing to it.
We can make another reference to a struct with the & symbol. It will be a non-owning reference. 3 This is called a lend.
In this example, the type of owningRef is Spaceship, and the type of nonOwningRef is &Spaceship.
struct Spaceship { name str; fuel int; }
exported func main() {
owningRef = Spaceship("Serenity", 2);
nonOwningRef = &owningRef;
}
More specifically, a constraint reference, see References.
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 {
name str;
fuel int;
}
exported func main() {
a = Spaceship("Serenity", 2); 4
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;
fuel int;
}
func foo(b Spaceship) {
println(b.name);
}
exported func main() {
a = Spaceship("Serenity", 2);
// Move the Spaceship from a
// into foo's b
foo(a);
// Can't use a now.
}
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).
When an owning reference disappears, the struct is automatically deallocated. Vale does this by inserting a call to drop. 5
The drop function is automatically generated for each struct and interface.
Here we can see where the implicit call to drop is.
drop is called when the owning reference goes away, which in this case is ship.
struct Spaceship {
name str;
fuel int;
}
exported func main() {
ship = Spaceship("Serenity", 2);
// ship is an owning reference
println(ship.name);
// implicit (ship).drop().
}
// Implicit:
// func drop(s Spaceship) {
// destruct s; 6 // frees
// }
If we specify #!DeriveStructDrop (which means "don't derive drop"), we can specify our own drop function instead. A custom drop could be used to:
Rule of thumb: if something must happen at some point in the future, put it in a drop function. Vale will make sure that it's not forgotten. 7
#!DeriveStructDrop
struct Spaceship {
name str;
fuel int;
}
exported func main() {
ship = Spaceship("Serenity", 2);
// ship is an owning reference
println(ship.name);
// implicit (ship).drop().
}
func drop(s Spaceship) {
// To destroy, move into a destructure.
[name, fuel] = s;
println("Destroyed {name}!");
}
Destroyed Serenity!
In other languages, drop has restrictions: it must take only a self parameter, and can't return anything.
In Vale, we have no such restrictions. drop can take parameters, can return anything, and doesn't even need to be name drop.
This example specifies #!DeriveStructDrop and instead defines a destroyShip function that takes a boolean parameter and returns an integer.
Since there is no drop function for the struct, Vale will never automatically destroy the object.
This is called Higher RAII, and it's a form of "linear typing" which we can use to enforce we call a certain function in the future.
#!DeriveStructDrop
struct Spaceship {
name str;
fuel int;
}
exported func main() {
ship = Spaceship("Serenity", 2);
// ship is an owning reference
println(ship.name);
fuel = (ship).destroyShip(true);
println("Fuel was {fuel}.")
}
func destroyShip(
s Spaceship,
print bool)
int {
[name, fuel] = s; // Deallocates ship
if print {
println("Destroyed {name}!");
}
return fuel;
}
Destroyed Serenity!
Fuel was 2.
See this article for more information on Higher RAII!
Drop functions also appear in C++ ("destructors") and Rust. Vale's drop functions are like those but more flexible: they can return values and even take extra parameters. In those cases, they must be called manually. See this article for more!
The destruct keyword is syntactic sugar for a "move destructure" pattern, see Pattern Matching for more.
This is an incredibly powerful pattern, see this article for more.
By default, structs are mutable. We can make immutable structs with the imm keyword.
After construction, an immutable struct cannot be changed at all.
Because of that, we can have multiple owning references to it, like Java or Python. 8
Vale also automatically derives the functions println, str, hash, ==, and more.
Immutable structs cannot have drop functions. 9
import stdlib.*;
struct Spaceship imm {
name str;
numWings int;
}
exported func main() {
ship = Spaceship("Serenity", 2);
ship2 = ship;
println(ship.numWings);
println(ship2.numWings);
}
2
2
Small immutable structs (32b or less) are copied and passed by-value. Larger objects use SNRC (strategic nonatomic reference counting) to free themselves.
See this article for the reasoning behind this.
A tuple is a simple struct, whose members are named 0, 1, 2, etc.
We can make a tuple in Vale with parentheses (like (5, true, 42)), and can access them with a dot like tup.0.
import stdlib.*;
exported func main() {
tup = (5, true, 42);
println("Babylon " + tup.0);
}
Babylon 5
These are planned features for Vale. See the roadmap for plans!
We normally call a function by name, such as the Spaceship("Serenity", 2) above. However, if the code is expecting a certain type, it can automatically call the constructor of the expected type.
// Using above Spaceship struct
exported func main() {
// These statements are equivalent:
x Spaceship = Spaceship("Raza", 2);
x Spaceship = ["Raza", 2];
}
This saves a lot of typing when calling functions.
// Using above Spaceship struct
func foo(s Spaceship) { ... }
exported func main() {
// These statements are equivalent:
foo(Spaceship("Raza", 2));
foo(["Raza", 2]);
}
We'll be able to produce a variant, which is something that can be one of two types (or more).
To make a variant, use an if-statement, and return a different type from the then branch than the else branch.
Here, a is a variant, either a string or an integer. a's type is (str|int).
exported func main() {
a =
if true { "hello" }
else { 42 };
}
Later, we can use the match statement to determine if a actually contains a str or an int.
We'll be able to access a arbitrary fields of tuples or structs with square brackets, like tup[1 + 1], which would produce a variant. 11
exported func main() {
tup = (5, true, "V");
println("Saturn " + tup[1 + 1]);
}
Saturn V
See this discussion for whether and how we might add shortcalling.
Specifically, indexing this tuple gives a variant (int|bool|str), with a + function that calls the appropriate actual + depending on the run-time type (int vs bool vs str).
Next: References