References
In the Ownership and Scope section, we explained that when a value is passed to a function, it is moved to the function's scope. This means that the function becomes the owner of the value, and the original scope (owner) can no longer use it. This is an important concept in Move, as it ensures that the value is not used in multiple places at the same time. However, there are use cases when we want to pass a value to a function but retain the ownership. This is where references come into play.
To illustrate this, let's consider a simple example - an application for a metro (subway) pass. We will look at 4 different scenarios:
- Card can be purchased at the kiosk for a fixed price
- Card can be shown to inspectors to prove that the passenger has a valid pass
- Card can be used at the turnstile to enter the metro, and spend a ride
- Card can be recycled once it's empty
Layout
The initial layout of the metro pass application is simple. We define the Card
type and the USES
constant that represents the number of rides for a single card. We also add an
error constant for the case when the card is empty.
module book::metro_pass {
/// Error code for when the card is empty.
const ENoUses: u64 = 0;
/// Number of uses for a metro pass card.
const USES: u8 = 3;
/// A metro pass card
public struct Card { uses: u8 }
/// Purchase a metro pass card.
public fun purchase(/* pass a Coin */): Card {
Card { uses: USES }
}
Reference
References are a way to show a value to a function without giving up the ownership. In our case, when we show the Card to the inspector, we don't want to give up the ownership of it, and we don't allow them to spend the rides. We just want to allow reading the value of the Card and prove its ownership.
To do so, in the function signature, we use the &
symbol to indicate that we are passing a
reference to the value, not the value itself.
/// Show the metro pass card to the inspector.
public fun is_valid(card: &Card): bool {
card.uses > 0
}
Now the function can't take the ownership of the card, and it can't spend the rides. But it can read its value. Worth noting, that a signature like this makes it impossible to call the function without a Card at all. This is an important property which allows the Capability Pattern which we will cover in the next chapters.
Mutable Reference
In some cases, we want to allow the function to change the value of the Card. For example, when we
use the Card at the turnstile, we want to spend a ride. To implement it, we use the &mut
keyword
in the function signature.
/// Use the metro pass card at the turnstile to enter the metro.
public fun enter_metro(card: &mut Card) {
assert!(card.uses > 0, ENoUses);
card.uses = card.uses - 1;
}
As you can see in the function body, the &mut
reference allows mutating the value, and the
function can spend the rides.
Passing by Value
Lastly, let's give an illustration of what happens when we pass the value itself to the function. In this case, the function takes the ownership of the value, and the original scope can no longer use it. The owner of the Card can recycle it, and, hence, lose the ownership.
/// Recycle the metro pass card.
public fun recycle(card: Card) {
assert!(card.uses == 0, ENoUses);
let Card { uses: _ } = card;
}
In the recycle
function, the Card is taken by value and can be unpacked and destroyed. The
original scope can't use it anymore.
Full Example
To illustrate the full flow of the application, let's put all the pieces together in a test.
#[test]
fun test_card_2024() {
// declaring variable as mutable because we modify it
let mut card = purchase();
card.enter_metro(); // modify the card but don't move it
assert!(card.is_valid()); // read the card!
card.enter_metro(); // modify the card but don't move it
card.enter_metro(); // modify the card but don't move it
card.recycle(); // move the card out of the scope
}