How does indexmap::IndexSet::get_full return both index and value unlike standard HashSet?

IndexSet::get_full returns a tuple (usize, &T) containing both the insertion-order index and a reference to the value, whereas HashSet::get only returns Option<&T>. This dual return is possible because IndexSet maintains insertion order in a contiguous array alongside its hash table—each element has a stable index that reflects when it was inserted. The method name "get_full" indicates that you get the "full" information about the element's position and value. This capability is fundamentally unavailable in standard HashSet because it doesn't track or expose insertion order; elements are stored according to their hash, and there's no meaningful index to return. When you need to know both "what is this value" and "when was it inserted," IndexSet::get_full provides both pieces of information in a single lookup.

Basic IndexSet get_full Usage

use indexmap::IndexSet;
 
fn main() {
    let mut set = IndexSet::new();
    
    set.insert("apple");
    set.insert("banana");
    set.insert("cherry");
    
    // get_full returns (index, &value) when found
    if let Some((index, value)) = set.get_full(&"banana") {
        println!("Found 'banana' at index {} with value '{}'", index, value);
        // Output: Found 'banana' at index 1 with value 'banana'
    }
    
    // Compare with standard HashSet behavior
    use std::collections::HashSet;
    let mut std_set = HashSet::new();
    std_set.insert("apple");
    std_set.insert("banana");
    std_set.insert("cherry");
    
    // HashSet::get only returns &T, no index
    if let Some(value) = std_set.get(&"banana") {
        println!("Found '{}' in HashSet (no index available)", value);
    }
    
    // Index 0: apple (first inserted)
    // Index 1: banana (second inserted)
    // Index 2: cherry (third inserted)
}

get_full returns the insertion-order index and a reference to the value; HashSet::get only returns the reference.

Understanding Insertion Order and Indices

use indexmap::IndexSet;
 
fn main() {
    let mut set = IndexSet::new();
    
    // Elements are indexed by insertion order
    set.insert("first");   // index 0
    set.insert("second");  // index 1
    set.insert("third");   // index 2
    
    // Indices are stable and reflect insertion order
    assert_eq!(set.get_full(&"first"), Some((0, &"first")));
    assert_eq!(set.get_full(&"second"), Some((1, &"second")));
    assert_eq!(set.get_full(&"third"), Some((2, &"third")));
    
    // If an element already exists, insert returns the existing index
    let existing_index = set.insert_full("second");
    assert_eq!(existing_index, (1, false));  // (index, inserted: false)
    
    // The index doesn't change for existing elements
    assert_eq!(set.get_full(&"second"), Some((1, &"second")));
    
    // Inserting new elements continues the index sequence
    set.insert("fourth");  // index 3
    assert_eq!(set.get_full(&"fourth"), Some((3, &"fourth")));
    
    println!("All elements in insertion order:");
    for (index, value) in set.iter().enumerate() {
        println!("  index {}: {}", index, value);
    }
}

Indices are assigned based on insertion order and remain stable for existing elements.

Comparison with Standard HashSet

use indexmap::IndexSet;
use std::collections::HashSet;
 
fn main() {
    // IndexSet: ordered, indexed
    let mut index_set = IndexSet::new();
    index_set.insert("a");
    index_set.insert("b");
    index_set.insert("c");
    
    // Can iterate in insertion order
    println!("IndexSet iteration order:");
    for (i, v) in index_set.iter().enumerate() {
        println!("  [{}] = {}", i, v);
    }
    // Always: a, b, c
    
    // Can get index
    let (idx, _) = index_set.get_full(&"b").unwrap();
    println!("'b' is at index {}", idx);  // Always 1
    
    // HashSet: unordered, no indices
    let mut hash_set = HashSet::new();
    hash_set.insert("a");
    hash_set.insert("b");
    hash_set.insert("c");
    
    // Iteration order is not guaranteed
    println!("\nHashSet iteration order (unspecified):");
    for v in &hash_set {
        println!("  {}", v);
    }
    // Order could be anything!
    
    // No way to get an index
    // hash_set.get(&"b") returns Option<&str>, no index
    // There's no equivalent to get_full
    
    // If you need the index in HashSet, you'd have to:
    // 1. Convert to a Vec and search (O(n))
    // 2. Maintain a separate index mapping (complex)
}

HashSet has no index concept; IndexSet maintains insertion order with stable indices.

Index-Based Operations

use indexmap::IndexSet;
 
fn main() {
    let mut set = IndexSet::new();
    set.insert("zero");
    set.insert("one");
    set.insert("two");
    set.insert("three");
    
    // get_full gives you the index, which enables index-based operations
    
    // 1. Remove by index (not possible with HashSet)
    let removed = set.swap_remove_index(1);
    println!("Removed at index 1: {:?}", removed);  // Some("one")
    
    // After removal, indices shift (swap_remove) or the element is swapped
    println!("After swap_remove:");
    for (i, v) in set.iter().enumerate() {
        println!("  [{}] = {}", i, v);
    }
    
    // 2. Get by index
    if let Some(value) = set.get_index(0) {
        println!("Value at index 0: {}", value);
    }
    
    // 3. Get both index and value from get_full
    if let Some((index, value)) = set.get_full(&"two") {
        println!("'two' is at index {} with value '{}'", index, value);
    }
    
    // 4. Use index for cross-referencing with other ordered data
    let metadata = vec!["meta-zero", "meta-one", "meta-two", "meta-three"];
    
    if let Some((index, _)) = set.get_full(&"two") {
        // Use the index to look up related data
        println!("Metadata for 'two': {}", metadata[index]);
    }
}

The index from get_full enables index-based operations like removal and cross-referencing.

Insert Full and Get Full Together

use indexmap::IndexSet;
 
fn main() {
    let mut set = IndexSet::new();
    
    // insert_full returns (index, inserted) 
    let (idx1, was_new1) = set.insert_full("first");
    println!("Inserted 'first' at index {}, was_new: {}", idx1, was_new1);
    
    let (idx2, was_new2) = set.insert_full("second");
    println!("Inserted 'second' at index {}, was_new: {}", idx2, was_new2);
    
    // Inserting duplicate returns existing index, inserted = false
    let (idx3, was_new3) = set.insert_full("first");
    println!("Tried 'first' again: index {}, was_new: {}", idx3, was_new3);
    // index is still 0, was_new is false
    
    // get_full confirms the same index
    if let Some((index, value)) = set.get_full(&"first") {
        assert_eq!(index, 0);
        assert_eq!(value, &"first");
    }
    
    // Pattern: use get_full to check if element exists and get its position
    if let Some((index, _)) = set.get_full(&"second") {
        println!("Found 'second' at index {}", index);
        // Now you can use index for other operations
    } else {
        // Element doesn't exist, could insert it
        set.insert("second");
    }
}

insert_full and get_full work together: both return the index for consistent position tracking.

Use Case: Ordered Position Lookup

use indexmap::IndexSet;
 
fn main() {
    // Scenario: Track the order in which unique users joined a chat
    let mut users = IndexSet::new();
    
    users.insert("alice");
    users.insert("bob");
    users.insert("charlie");
    users.insert("diana");
    
    // Later, find out when a user joined
    fn get_join_position(users: &IndexSet<&str>, username: &str) -> Option<usize> {
        users.get_full(username).map(|(index, _)| index)
    }
    
    println!("Alice joined at position: {:?}", get_join_position(&users, "alice"));
    println!("Bob joined at position: {:?}", get_join_position(&users, "bob"));
    println!("Eve joined at position: {:?}", get_join_position(&users, "eve"));  // None
    
    // First user has index 0, second has 1, etc.
    // The index tells you the relative join order
    
    // This is impossible with HashSet without additional data structures
}

get_full enables position-based queries that would require extra bookkeeping with HashSet.

Use Case: Index-Based Slicing

use indexmap::IndexSet;
 
fn main() {
    // Scenario: Maintain ordered unique items and slice by index range
    let mut items = IndexSet::new();
    
    for i in 0..10 {
        items.insert(format!("item-{}", i));
    }
    
    // Find an item and get everything after it
    if let Some((start_index, _)) = items.get_full(&"item-3".to_string()) {
        println!("Items from 'item-3' onwards:");
        for (i, item) in items.iter().enumerate().skip(start_index) {
            println!("  [{}] {}", i, item);
        }
    }
    
    // Get items before a certain point
    if let Some((end_index, _)) = items.get_full(&"item-7".to_string()) {
        println!("\nItems before 'item-7':");
        for (i, item) in items.iter().enumerate().take(end_index) {
            println!("  [{}] {}", i, item);
        }
    }
    
    // With HashSet, you'd need to collect into a Vec first
    // and then search (O(n) instead of O(1))
}

The index from get_full enables range operations relative to an element's position.

Use Case: Cross-Referencing Ordered Data

use indexmap::IndexSet;
 
fn main() {
    // Scenario: Ordered set of column names with parallel data vectors
    let mut columns = IndexSet::new();
    columns.insert("name");
    columns.insert("age");
    columns.insert("email");
    
    // Parallel data storage indexed by column position
    let mut data: Vec<Vec<String>> = vec![
        vec!["Alice".to_string(), "Bob".to_string()],
        vec!["30".to_string(), "25".to_string()],
        vec!["alice@example.com".to_string(), "bob@example.com".to_string()],
    ];
    
    // Query: get the age column data
    if let Some((age_idx, col_name)) = columns.get_full(&"age") {
        println!("Column '{}' at index {}: {:?}", col_name, age_idx, data[age_idx]);
    }
    
    // Query: find which column a value belongs to
    fn find_column_for_value(
        columns: &IndexSet<&str>,
        data: &[Vec<String>],
        value: &str
    ) -> Option<(&str, usize)> {
        for (idx, col_name) in columns.iter().enumerate() {
            if data[idx].iter().any(|v| v == value) {
                return Some((*col_name, idx));
            }
        }
        None
    }
    
    if let Some((col, idx)) = find_column_for_value(&columns, &data, "Bob") {
        println!("Found 'Bob' in column '{}' (index {})", col, idx);
    }
}

get_full provides the index needed to cross-reference between ordered sets and parallel arrays.

Binary Search on Sorted IndexSet

use indexmap::IndexSet;
 
fn main() {
    // IndexSet maintains insertion order, not sorted order
    let mut set = IndexSet::new();
    set.insert("cherry");
    set.insert("apple");
    set.insert("banana");
    
    // Iteration order is insertion order, not sorted
    println!("Insertion order:");
    for v in &set {
        println!("  {}", v);  // cherry, apple, banana
    }
    
    // If you need sorted access, you can:
    // 1. Get indices and sort by values
    let mut indices: Vec<usize> = (0..set.len()).collect();
    indices.sort_by_key(|&i| set.get_index(i).unwrap());
    
    println!("\nSorted access using indices:");
    for i in indices {
        println!("  [{}] {}", i, set.get_index(i).unwrap());
    }
    
    // 2. Use get_full in combination with binary search on a sorted view
    let sorted_values: Vec<&&str> = set.iter().sorted().collect();
    
    // Find in sorted view, then get original index
    let target = "banana";
    if let Ok(pos) = sorted_values.binary_search(&&target) {
        let value = sorted_values[pos];
        if let Some((original_index, _)) = set.get_full(*value) {
            println!("\n'{}' found at sorted position {}, original index {}",
                target, pos, original_index);
        }
    }
}
 
// Helper trait for sorted iteration
trait SortedIter<'a, T: 'a + Ord>: Iterator<Item = &'a T> + Sized {
    fn sorted(self) -> std::vec::IntoIter<&'a T> {
        let mut v: Vec<&'a T> = self.collect();
        v.sort();
        v.into_iter()
    }
}
 
impl<'a, T: 'a + Ord, I: Iterator<Item = &'a T>> SortedIter<'a, T> for I {}

IndexSet maintains insertion order; combine get_full with sorting for ordered access patterns.

Memory and Performance Characteristics

use indexmap::IndexSet;
use std::collections::HashSet;
 
fn main() {
    // Memory: IndexSet stores both a hash table and a contiguous array
    // HashSet stores only a hash table
    
    // Performance comparison:
    // IndexSet::get_full: O(1) average, returns (index, &T)
    // HashSet::get: O(1) average, returns &T only
    
    let mut index_set = IndexSet::new();
    let mut hash_set = HashSet::new();
    
    for i in 0..1000 {
        index_set.insert(i);
        hash_set.insert(i);
    }
    
    // Both are O(1) for lookup
    // But IndexSet gives you the index for free
    
    let start = std::time::Instant::now();
    for i in 0..1000 {
        let _ = index_set.get_full(&i);
    }
    println!("IndexSet::get_full x1000: {:?}", start.elapsed());
    
    let start = std::time::Instant::now();
    for i in 0..1000 {
        let _ = hash_set.get(&i);
    }
    println!("HashSet::get x1000: {:?}", start.elapsed());
    
    // Memory overhead:
    // IndexSet: additional Vec<usize> for indices
    // HashSet: just the hash table
    
    println!("\nIndexSet memory is slightly higher due to index storage");
    println!("Trade-off: extra memory for ordered iteration and indices");
}

IndexSet has slightly higher memory overhead but provides indices and ordered iteration.

API Comparison

use indexmap::IndexSet;
use std::collections::HashSet;
 
fn main() {
    let mut index_set = IndexSet::new();
    index_set.insert("a");
    index_set.insert("b");
    index_set.insert("c");
    
    let mut hash_set = HashSet::new();
    hash_set.insert("a");
    hash_set.insert("b");
    hash_set.insert("c");
    
    // IndexSet methods that return index:
    
    // get_full: returns Option<(usize, &T)>
    println!("IndexSet::get_full: {:?}", index_set.get_full(&"b"));
    // Some((1, "b"))
    
    // get_index: returns Option<&T> by index
    println!("IndexSet::get_index: {:?}", index_set.get_index(1));
    // Some("b")
    
    // insert_full: returns (usize, bool) - index and whether inserted
    let (idx, inserted) = index_set.insert_full("d");
    println!("IndexSet::insert_full: index={}, inserted={}", idx, inserted);
    // index=3, inserted=true
    
    // HashSet methods:
    
    // get: returns Option<&T> - no index
    println!("HashSet::get: {:?}", hash_set.get(&"b"));
    // Some(&"b")
    
    // insert: returns bool - was it new?
    let inserted = hash_set.insert("d");
    println!("HashSet::insert: inserted={}", inserted);
    // inserted=true
    
    // No equivalent to get_full, get_index, or insert_full in HashSet
}

IndexSet provides index-aware variants of get and insert that HashSet cannot offer.

Synthesis

Method comparison:

Operation IndexSet HashSet
Get value get(&T) -> Option<&T> get(&T) -> Option<&T>
Get with index get_full(&T) -> Option<(usize, &T)> Not available
Get by index get_index(usize) -> Option<&T> Not available
Insert insert(T) -> bool insert(T) -> bool
Insert with index insert_full(T) -> (usize, bool) Not available
Remove remove(&T) -> bool remove(&T) -> bool
Remove by index swap_remove_index(usize) -> Option<T> Not available
Iterate Ordered by insertion Unspecified order

When IndexSet::get_full is valuable:

Use Case Benefit
Position tracking Know when an element was inserted
Cross-referencing Index into parallel arrays/vectors
Ordered slicing Get elements before/after a found element
Index-based operations Use index for removal or other operations
Two-way lookup Find element by value, use index for related data

Performance comparison:

Operation IndexSet HashSet
Lookup O(1) average O(1) average
Insert O(1) average O(1) average
Iteration O(n), ordered O(n), unordered
Memory Higher (array + hash table) Lower (hash table only)

Key insight: IndexSet::get_full bridges the gap between set semantics (uniqueness) and sequence semantics (ordered positions). Standard HashSet provides uniqueness with O(1) lookups but no positional information—elements exist in an unspecified hash table order, and there's no stable index associated with each element. IndexSet adds a contiguous array alongside its hash table, where each element's position in the array reflects its insertion order. This dual structure enables get_full to return both the index (position in the array) and the value (reference to the element) in a single O(1) operation. The index is meaningful: it tells you the element's relative insertion order, enables cross-referencing with other ordered data structures, and allows index-based operations like get_index and swap_remove_index. The trade-off is slightly higher memory usage and the constraint that indices can change when elements are removed (unless you use shift_remove which shifts all subsequent elements). For any application where you need both uniqueness guarantees and position tracking—such as ordered unique lists, column headers in data tables, or tracking join order in a chat room—IndexSet::get_full provides information that would require additional bookkeeping data structures with HashSet.