Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.collections

from std.collections import Heap, HeapBy, drain, drain_by provides owned binary heaps (priority queues). They are ordinary embedded Plenty code over a private list[T]; no native code or runtime primitive is involved.

TypeFirst elementElement types
Heap[T]Least by <Integers, floats, str, and tuples of those
HeapBy[T, F]The one the comparator puts before all othersAny storable type

Heap[T] uses the element type’s own <. Classes, ordinary enums, collections, and tuples containing unordered fields are rejected at compile time with “operator < does not accept” the type. A scalar NaN element compares false against everything and leaves the removal order unspecified. Tuples use the lexicographic total sorting order, including signed zeros and NaNs, so Heap[(i64, str)] orders by priority then text.

HeapBy(before) stores a comparator F: Callable[[&T, &T], bool]: a named function or an owned closure. before(a, b) returns whether a must leave ahead of b; it must be a consistent strict order and must return False for equal elements. Reverse the test for a greatest-first queue. The comparator is inferred from its signature, as in HeapBy(urgent)?. A closure with borrowed captures cannot be stored.

from std.collections import Heap, drain

def main() -> Result[(), AllocError | IoError]:
    mut tasks = Heap[(i64, str)]()?
    tasks.push((3, "write docs"))?
    tasks.push((1, "fix crash"))?
    tasks.push((2, "review"))?
    print(tasks.peek())?
    for priority, name in drain(tasks):
        print(priority, name)?
    Ok(())
Option[tuple[i64, str]].Some((1, "fix crash"))
1 fix crash
2 review
3 write docs

Operations

Both types share one interface. heap is Heap[T] or HeapBy[T, F].

OperationResultContract
Heap[T]()Result[Heap[T], AllocError]Construct an empty heap
HeapBy(before)Result[HeapBy[T, F], AllocError]Construct an empty heap owning its comparator
heap.len()i64Observe the element count
heap.is_empty()boolObserve whether the count is zero
heap.reserve(additional)Result[(), AllocError]Make room for at least additional: i64 more elements
heap.push(value)Result[(), AllocError]Consume a value and insert it
heap.peek()Option[T]Copy the first element; copyable T only
heap.peek_ref()Option[&T]Borrow the first element
heap.pop()Option[T]Remove and transfer the first element
heap.clear()()Destroy all elements, retaining capacity
drain(heap)Generator[T]Consume a Heap[T], yielding elements in removal order
drain_by(heap)Generator[T]The same for a HeapBy[T, F]

Mutation requires a mutable owner or exclusive reference. On an empty heap, pop, peek and peek_ref return Nothing. peek follows the copying lookup rule: it type-checks only for a copyable T, and is checked where it is called, so a heap of class instances uses peek_ref. The reference from peek_ref borrows the heap, so the heap cannot change while it is live. There is no peek_mut: changing the first element in place could break the heap order.

Elements that compare equal leave in an unspecified order. For first-in, first-out ties, store an increasing sequence number in the element and compare it after the priority.

Ownership and cleanup

A heap owns its elements and moves like any other owner; duplicate it only with an explicit copy. push takes ownership, and pop transfers it back. Reordering exchanges rows in place: no element is copied, retained, or dropped while it is in the heap. References, generators, and unit cannot be elements.

Dropping or clearing a heap destroys the remaining elements once each, in storage order, not priority order. A drain frame owns the heap: abandoning the frame early destroys the elements not yet yielded.

A heap holds no OS resource and no shared state. It has no thread-safety contract of its own; it is an ordinary owned value of its element type.

Errors and costs

AllocError is the only error. push allocates only when the backing list must grow and reserve only when the requested room is missing. On failure both leave the existing contents and order unchanged, and push drops the incoming value. Every other operation, including drain, allocates nothing.

OperationTime for n elements
len, is_empty, peek, peek_refConstant
pushAt most floor(log2 n) comparisons; a growing push also moves the backing list
popAt most 2 * floor(log2 n) comparisons
clear, dropLinear, plus element cleanup
Draining all elementsO(n log n) comparisons

Supported target: x86_64-unknown-linux-gnu. See the runnable priority queue lesson.

Limits

There is no construction from an existing list, bulk insertion, merging, indexed access, borrowed iteration, removal of an arbitrary element, or priority update. A comparator cannot fail; one that reports inconsistent answers gives an unspecified order but cannot break memory safety.

Acknowledgements

  • Modelled on Python’s heapq, which also puts the least element first, and Rust’s BinaryHeap.