The previous lessons introduced list values and types. Now it is time to use lists as part of a program: inspect their entries, update them, combine them, and derive new lists from them.
Start with a small inventory:
let inventory = ("Map", "Torch", "Key");
Sugar infers that inventory is a list of strings. An explicit List<str> annotation is only needed when Sugar cannot infer the entry type, such as when a list begins empty.
Most list operations use method syntax:
inventory.len()
inventory.push("Potion")
The list before the dot is the receiver. The method either reads that list, changes it, or produces a new value from it.
Read entries by position
List indexes begin at 1, so the first entry is at index 1:
inventory[1] // "Map"
inventory[2] // "Torch"
The index can also come from an expression:
let position = 3;
inventory[position] // "Key"
Reading beyond either end of a list returns None. A non-positive or non-whole index is invalid.
inventory[20] // None
Use len() to count the entries:
inventory.len() // 3
Read the first or last entry
first() and last() avoid writing an index when the position itself is what matters:
inventory.first() // "Map"
inventory.last() // "Key"
Either method returns None when the list is empty:
let empty: List<str> = ();
empty.first() // None
empty.last() // None
These methods only inspect the list. They never remove an entry.
Check whether a value is present
contains() returns true when an equal value appears anywhere in the list:
inventory.contains("Torch") // true
inventory.contains("Crown") // false
Sugar also has the in and not in operators. Choose whichever form reads most naturally:
"Torch" in inventory
"Crown" not in inventory
contains() and in perform the same membership check. Neither changes the list.
Add an entry with push()
push(value) adds one entry to the end of the list:
inventory.push("Potion");
Afterward, inventory contains:
("Map", "Torch", "Key", "Potion")
The new value must match the list’s entry type. A list of strings accepts another string, but not a number:
inventory.push(10) // Error: expected str
push() returns None. Its result is the change made to inventory, not a new list.
Insert an entry at a position
insert(index, value) places a value before the entry currently at that index:
inventory.insert(2, "Compass");
The list is now:
("Map", "Compass", "Torch", "Key", "Potion")
Like list access, insertion uses one-based indexes. Index 1 inserts at the beginning. To insert at the end, use one position after the current final entry:
inventory.insert(inventory.len() + 1, "Rope");
That last form is valid, although push("Rope") is simpler when the goal is only to append.
An insertion index must be between 1 and list.len() + 1. Anything outside that range is an error. insert() returns None.
Remove entries with pop() and remove()
pop() removes the final entry and returns it:
let packedLast = inventory.pop();
If the final entry was "Rope", packedLast is now "Rope", and "Rope" is no longer in inventory.
An empty list has nothing to remove, so pop() returns None:
let empty: List<str> = ();
empty.pop() // None
Use remove(index) when the entry’s position is known:
let discarded = inventory.remove(2);
remove(2) removes and returns the second entry. Entries after it shift one position toward the beginning. Unlike reading an absent index, removing an index that does not exist is an error; it usually signals a mistake in the program.
Empty a list with clear()
clear() removes every entry:
inventory.clear();
inventory.len() // 0
The list keeps its entry type, so it can still accept another string later:
inventory.push("Fresh start");
clear() returns None.
Know which methods change the list
Five list methods mutate, or directly change, their receiver:
| Method | Change | Result |
|---|---|---|
push(value) | Add value at the end | None |
insert(index, value) | Add value before index | None |
pop() | Remove the final entry | Removed entry, or None when empty |
remove(index) | Remove the entry at index | Removed entry |
clear() | Remove every entry | None |
Because these methods change something, their receiver must name a mutable list:
let inventory = ("Map",);
inventory.push("Torch"); // Valid
A temporary list or a list produced by another operation has no mutable name to update:
("Map", "Torch").push("Key") // Error
inventory.filter(|item| true).clear() // Error
Constants cannot be mutated either.
The receiver may be a nested list stored inside a mutable value:
let character = {
name: "Mina",
inventory: ("Map",),
};
character.inventory.push("Torch");
Lists are copied as values
Assigning a list copies its value. Mutating the copy does not reach backward and change the original:
let original = ("Map", "Torch");
let packed = original;
packed.push("Potion");
After the call, the two lists are different:
original // ("Map", "Torch")
packed // ("Map", "Torch", "Potion")
This also applies to lists passed into a function. A function may mutate its local parameter, but the caller’s list remains unchanged unless the returned list is assigned back.
fn withPotion(items: List<str>) {
items.push("Potion");
items
}
inventory = withPotion(inventory);
Include entries conditionally
Mutation is not always necessary. When constructing a list, add when after an entry that should only be present under a condition:
let rewards = (
"Coins",
"Bonus" when score >= 100,
"Perfect" when score == maximumScore,
);
Sugar checks each condition as it creates the list. A false condition leaves out that entry. Conditional entries work with every list entry type, including the lists of Media used as Media Pools.
Combine lists with +
The + operator joins two lists in order:
let commonRewards = ("Coins", "Potion");
let rareRewards = ("Crown", "Dragon Egg");
let allRewards = commonRewards + rareRewards;
allRewards contains all four entries. commonRewards and rareRewards remain unchanged because + produces a new list.
Remove matching values with -
The - operator produces a new list without values found in the list on its right:
let blockedRewards = ("Potion", "Dragon Egg");
let availableRewards = allRewards - blockedRewards;
availableRewards contains "Coins" and "Crown". Every matching occurrence is removed, while the remaining entries keep their order.
To subtract one value, place it in a one-entry list. Remember its trailing comma:
let withoutCoins = allRewards - ("Coins",);
Use remove(index) to mutate a list by position. Use - to create a new list by matching values.
Work with each entry using a closure
The remaining methods receive an Fn closure. Sugar calls that closure once for each entry, in list order.
These examples use a list of player objects:
let players = (
{ name: "Mina", score: 120, ready: true },
{ name: "Ivo", score: 70, ready: false },
{ name: "Noor", score: 105, ready: true },
);
In |player| ..., player is the current entry:
players.filter(|player| player.ready)
The closure does not need to be named unless it will be reused.
Keep matching entries with filter()
filter() keeps every entry for which its closure returns true:
let finalists = players.filter(|player| player.score >= 100);
finalists contains Mina and Noor. players is unchanged; filter() returns a new list.
A stored closure works too:
let isReady: Fn = |player| player.ready;
let readyPlayers = players.filter(isReady);
Find one matching entry with find()
find() stops at the first entry for which its closure returns true:
let firstFinalist = players.find(|player| player.score >= 100);
This returns Mina’s object. If nobody matches, it returns None.
Use find() when the program needs one possible entry. Use filter() when it needs a list containing every match.
Transform entries with map()
map() replaces each entry with the value returned by its closure:
let names = players.map(|player| player.name);
names becomes:
("Mina", "Ivo", "Noor")
The result’s entry type may differ from the source. Here a list of objects becomes a list of strings.
Ask a group question with any() and all()
any() returns true when at least one entry matches:
let someoneQualified = players.any(|player| player.score >= 100);
all() returns true only when every entry matches:
let everyoneReady = players.all(|player| player.ready);
Unlike filter(), these methods return one boolean rather than a list.
Keep entries from the beginning with take()
take(number) returns up to that many entries from the beginning:
let firstTwo = players.take(2);
Its range form expresses both a required minimum and an allowed maximum:
let availableTeam = players
.filter(|player| player.ready)
.take(1..=3);
This requires at least one ready player and keeps no more than three. take(3) only sets the maximum.
Chain operations into a pipeline
Methods that return a list can be followed by another list method:
let finalistNames = players
.filter(|player| player.score >= 100)
.map(|player| player.name)
.take(2);
Read this from top to bottom:
filter()keeps the qualifying players.map()turns those player objects into names.take()keeps the first two names.
Each of these methods creates a new list, so the original players list remains unchanged.
Keep functions in a list
Closures are ordinary Fn values, so an execution-local list can store them:
let double: Fn = |value| value * 2;
let triple: Fn = |value| value * 3;
let operations: List<Fn> = (double, triple);
operations[2](4) // 12
Fn values are execution-local. A List<Fn> cannot be stored in Memory or App state.
Choose the operation that matches the intent
| Goal | Operation |
|---|---|
| Read a known position | list[index] |
| Read an edge safely | first() or last() |
| Test membership | contains(), in, or not in |
| Add or remove entries in the existing list | push(), insert(), pop(), remove(), or clear() |
| Join lists into a new list | + |
| Exclude matching values from a new list | - |
| Keep every matching entry | filter() |
| Get the first matching entry | find() |
| Turn every entry into another value | map() |
| Ask whether some or all entries match | any() or all() |
| Keep a limited beginning of a list | take() |
The important distinction is whether the operation changes its receiver. The five mutation methods update a named mutable list. Reading, membership, operators, and closure-based transformations leave their source alone.