Skip to main content

Dynamic Collections

Sui Framework offers a variety of collection types that build on the dynamic fields and dynamic object fields concepts. These collections are designed to be a safer and more understandable way to store and manage dynamic fields and objects.

For each collection type we will specify the primitive they use, and the specific features they offer.

Unlike dynamic (object) fields which operate on UID, collection types have their own type and allow calling associated functions.

Common Concepts

All of the collection types share the same set of methods, which are:

  • add - adds a field to the collection
  • remove - removes a field from the collection
  • borrow - borrows a field from the collection
  • borrow_mut - borrows a mutable reference to a field from the collection
  • contains - checks if a field exists in the collection
  • length - returns the number of fields in the collection
  • is_empty - checks if the length is 0

All collection types support index syntax for borrow and borrow_mut methods. If you see square brackets in the examples, they are translated into borrow and borrow_mut calls.

let hat: &Hat = &bag[b"key"];
let hat_mut: &mut Hat = &mut bag[b"key"];

// is equivalent to
let hat: &Hat = bag.borrow(b"key");
let hat_mut: &mut Hat = bag.borrow_mut(b"key");

In the examples we won't focus on these functions, but rather on the differences between the collection types.

Bag

Bag, as the name suggests, acts as a "bag" of heterogeneous values. It is a simple, non-generic type that can store any data. Bag will never allow orphaned fields, as it tracks the number of fields and can't be destroyed if it's not empty.

module sui::bag;

public struct Bag has key, store {
/// the ID of this bag
id: UID,
/// the number of key-value pairs in the bag
size: u64,
}

See full documentation for sui::bag module.

Due to Bag storing any types, the extra methods it offers is:

  • contains_with_type - checks if a field exists with a specific type

Used as a struct field:

/// Imported from the `sui::bag` module.
use sui::bag::{Self, Bag};

/// An example of a `Bag` as a struct field.
public struct Carrier has key {
id: UID,
bag: Bag
}

Using the Bag:

let mut bag = bag::new(ctx);

// bag has the `length` function to get the number of elements
assert!(bag.length() == 0);

bag.add(b"my_key", b"my_value".to_string());

// length has changed to 1
assert!(bag.length() == 1);

// in order: `borrow`, `borrow_mut` and `remove`
// the value type must be specified
let field_ref: &String = &bag[b"my_key"];
let field_mut: &mut String = &mut bag[b"my_key"];
let field: String = bag.remove(b"my_key");

// length is back to 0 - we can unpack
bag.destroy_empty();

ObjectBag

Defined in the sui::object_bag module. Identical to Bag, but uses dynamic object fields internally. Can only store objects as values.

See full documentation for sui::object_bag module.

Table

Table is a typed dynamic collection that has a fixed type for keys and values. It is defined in the sui::table module.

module sui::table;

public struct Table<phantom K: copy + drop + store, phantom V: store> has key, store {
/// the ID of this table
id: UID,
/// the number of key-value pairs in the table
size: u64,
}

See full documentation for sui::table module.

Used as a struct field:

/// Imported from the `sui::table` module.
use sui::table::{Self, Table};

/// Some record type with `store`
public struct Record has store { /* ... */ }

/// An example of a `Table` as a struct field.
public struct UserRegistry has key {
id: UID,
table: Table<address, Record>
}

Using the Table:

// Table requires explicit type parameters for the key and value
// ...but does it only once in initialization.
let mut table = table::new<address, String>(ctx);

// table has the `length` function to get the number of elements
assert!(table.length() == 0);

table.add(@0xa11ce, b"my_value".to_string());
table.add(@0xb0b, b"another_value".to_string());

// length has changed to 2
assert!(table.length() == 2);

// in order: `borrow`, `borrow_mut` and `remove`
let value_ref = &table[@0xa11ce];
let value_mut = &mut table[@0xa11ce];

// removing both values
let _value = table.remove(@0xa11ce);
let _another_value = table.remove(@0xb0b);

// length is back to 0 - we can unpack
table.destroy_empty();

ObjectTable

Defined in the sui::object_table module. Identical to Table, but uses dynamic object fields internally. Can only store objects as values.

See full documentation for sui::object_table module.

LinkedTable

It is defined in the sui::linked_table module, similar to Table but the values are linked together, allowing for ordered insertion and removal.

module sui::linked_table;

public struct LinkedTable<K: copy + drop + store, phantom V: store> has key, store {
/// the ID of this table
id: UID,
/// the number of key-value pairs in the table
size: u64,
/// the front of the table, i.e. the key of the first entry
head: Option<K>,
/// the back of the table, i.e. the key of the last entry
tail: Option<K>,
}

See full documentation for sui::linked_table module.

Since the values stored in LinkedTable are linked together, it has unique methods for adding and deleting.

  • push_front - inserts a key-value pair at the front of the table
  • push_back - inserts a key-value pair at the back of the table
  • remove - removes a key-value pair by key and returns the value
  • pop_front - removes the front of the table, returns the key and value
  • pop_back - removes the back of the table, returns the key and value

Used as a struct field:

/// Imported from the `sui::linked_table` module.
use sui::linked_table::{Self, LinkedTable};

/// Some record type with `store`
public struct Permissions has store { /* ... */ }

/// An example of a `LinkedTable` as a struct field.
public struct AdminRegistry has key {
id: UID,
linked_table: LinkedTable<address, Permissions>
}

Using the LinkedTable:

// LinkedTable requires explicit type parameters for the key and value
// ...but does it only once in initialization.
let mut linked_table = linked_table::new<address, String>(ctx);

// linked_table has the `length` function to get the number of elements
assert!(linked_table.length() == 0);

linked_table.push_front(@0xa0a, b"first_value".to_string());
linked_table.push_back(@0xb1b, b"second_value".to_string());
linked_table.push_back(@0xc2c, b"third_value".to_string());

// length has changed to 3
assert!(linked_table.length() == 3);

// in order: `borrow`, `borrow_mut` and `remove`
let first_value_ref = &linked_table[@0xa0a];
let second_value_mut = &mut linked_table[@0xb1b];

// remove by key, from the beginning or from the end
let _second_value = linked_table.remove(@0xb1b);
let (_first_addr, _first_value) = linked_table.pop_front();
let (_third_addr, _third_value) = linked_table.pop_back();

// length is back to 0 - we can unpack
linked_table.destroy_empty();

Summary

  • Bag - a simple collection that can store any type of data.
  • ObjectBag - a collection that can store only objects.
  • Table - a typed dynamic collection that has a fixed type for keys and values.
  • ObjectTable - same as Table, but can only store objects.
  • LinkedTable - similar to Table but the values are linked together.

Further Reading