References
In the Ownership and Scope section, we explained that when a value is passed to a function, the value's ownership is transferred to the function. 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 card that can be:
- Purchased at the kiosk for a fixed price.
- Shown to inspectors to prove that the passenger has a valid pass.
- Used at the turnstile to enter the metro, and spend a ride.
- 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.
/// 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. It's 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.
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 including some tests.
// Copyright (c) Mysten Labs, Inc.
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
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 }
}
/// Show the metro pass card to the inspector.
public fun is_valid(card: &Card): bool {
card.uses > 0
}
/// 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;
}
/// Recycle the metro pass card.
public fun recycle(card: Card) {
assert!(card.uses == 0, ENoUses);
let Card { uses: _ } = card;
}
#[test]
fun test_card() {
// declaring variable as mutable because we modify it
let mut card = purchase();
enter_metro(&mut card);
assert!(is_valid(&card)); // read the card!
enter_metro(&mut card); // modify the card but don't move it
enter_metro(&mut card); // modify the card but don't move it
recycle(card); // move the card out of the scope
}
#[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
}
}