References
A reference is just what it sounds like: a reference to memory. C/C++ programmers can think of them mostly as pointers, except they must always be valid and arithmetic is not allowed.
Creating References
References are created using the &
modifier:
fn main() {
var charlie: Person("Charlie", 33) # Immutable
var hillary: Person("Hillary", 68) # Immutable
var barack: Person("Barack", 55) # Immutable
var charef: &charlie
charlie.age++
}
Unlike pointers in C/C++, references cannot refer to other references:
fn main() {
var charlie: Person("Charlie", 33) # Immutable
var hillary: Person("Hillary", 68) # Immutable
var barack: Person("Barack", 55) # Immutable
var charef: &charlie
var charinception: &charef # Compile error!
charlie.age++
}
This is because without pointer arithmetic and manual dereferencing (Ki dereferences automatically) there is no use for references to references.
Using References
References are very useful. Let's create a short example:
fn have_a_birthday(Person p): (Person) {
p.age++
return(p)
}
fn main() {
var charlie: Person("Charlie", 33) # Immutable
var hillary: Person("Hillary", 68) # Immutable
var barack: Person("Barack", 55) # Immutable
charlie = have_a_birthday(charlie)
}
This seems pretty straightforward, if somewhat contrived. What may be
surprising, however, is that p
is passed by value, meaning it is copied
whenever have_a_birthday
is called. Even worse, whenever have_a_birthday
returns, it creates another copy. This is highly inefficient, especially
for large types.
References can avoid these copies, however:
fn have_a_birthday(Person &p!) {
p.age++
}
fn main() {
var charlie: Person("Charlie", 33) # Immutable
var hillary: Person("Hillary", 68) # Immutable
var barack: Person("Barack", 55) # Immutable
have_a_birthday(&charlie!)
}
Much better. Note that we no longer have to reassign charlie
, as the memory
is being mutated in-place.
Speaking of mutation, there are two types of references: immutable and
mutable. Because have_a_birthday
mutates its Person
reference, we need to
mark that parameter as mutable using !
and also pass it a mutable reference,
again using !
. Failure to do either of these will result in a compile-time
error.
For more information on mutability, see here.
Guarantees
References play a key role in Ki's safety mechanisms, and they provide certain guarantees.
If A Mutable Reference To Memory Exists, No Other References To That Memory Exist.
Similar to Rust, Ki imposes this restriction to prevent data races: where one part of the code attempts to read a portion of memory while another part of the code attempts to write to it.
This also means that mutable references may not be copied. They can, however, be moved.
References Are Always Valid
This guarantee requires some constraints:
References May Not Be Stored In A struct
Or type
Because the memory underneath the reference may be deallocated before the
struct
/type
is, references may not be stored in them.
References May Not Be Passed To A gen
Or async
Function
This is for the same reason as the struct
/type
restriction, however, the
&self
reference is an exception.
Return Values May Not Be References
This is to prevent returning references to stack-allocated memory.
Moving Mutable References
It is sometimes helpful to pass mutable references from function to function.
However, because there can only be one mutable reference to memory at a time,
it cannot be copied (passed by value). It can also not be passed by reference,
because we cannot take the reference of a reference. The only option left to us is to move the reference using *
:
fn make_senior_citizen(Person &p!) {
p.name = "Old " + p.name # Note that you'll get a **lot** of Old's...
}
fn have_a_birthday(Person &p!) {
p.age++
if (p.age > 30) {
make_senior_citizen(*p)
# Trying to use p after this point will result in a compiler error
}
# But using p again here is fine
}
fn main() {
var charlie: Person("Charlie", 33) # Immutable
var hillary: Person("Hillary", 68) # Immutable
var barack: Person("Barack", 55) # Immutable
have_a_birthday(&charlie!)
}
Getting A Reference Back
Because you can either have many immutable references or a single mutable reference, it's important to be able to keep track of them. The various restrictions Ki places on references are designed to ensure that references can always travel back up in scope. The last example can serve as an example of this as well:
fn make_senior_citizen(Person &p!) {
p.name = "Old " + p.name # Note that you'll get a **lot** of Old's...
}
fn have_a_birthday(Person &p!) {
p.age++
if (p.age > 30) {
make_senior_citizen(p!) # Move p (&charlie) down into `make_senior_citizen`'s scope
# Could use `p!` again here because `make_senior_citizen` no longer has
# the mutable reference
}
}
fn main() {
var charlie: Person("Charlie", 33) # Immutable
var hillary: Person("Hillary", 68) # Immutable
var barack: Person("Barack", 55) # Immutable
have_a_birthday(&charlie!) # Move &charlie down into `have_a_birthday`'s scope
}
This is essentially why references cannot be stored in struct
s or type
s,
and why they cannot be passed to gen
/async
functions: allowing these would
allow references to be trapped in different scopes, which requires much more
complicated bookkeeping (Rust's borrow checker, for example) to verify.