The :: (Path Separator) Notation
In Rust, :: is used to access items in modules, crates, and namespaces. Think of it like navigating a file system, but for code organization.
// Accessing standard library items
std::collections::HashMap
std::io::stdin
// Accessing associated functions (like static methods)
String::from("hello")
Vec::new()
// Accessing associated constants
i32::MAX
f64::EPSILON
The :: operator creates a path through the module hierarchy. std::collections::HashMap means: “In the std crate, go to the collections module, and get HashMap”.
Importing with use
The use keyword brings items into scope so you don’t have to write the full path every time.
// Without use - verbose
let map = std::collections::HashMap::new();
// With use - cleaner
use std::collections::HashMap;
let map = HashMap::new();
// You can also use nested paths
use std::collections::{HashMap, HashSet};
use std::io::{self, Read, Write}; // 'self' brings the parent module too
Key points:
use statements typically go at the top of the file
- You can use
as to rename imports: use std::io::Result as IoResult;, similar to python
- Wildcards exist but are discouraged:
use std::collections::*;
The Standard Library (std)
Rust’s standard library (std) provides essential functionality without external dependencies. It’s always available and includes:
- Collections:
Vec, HashMap, HashSet, BTreeMap
- I/O:
std::io for reading/writing, std::fs for file operations
- String handling:
String, str, String::from()
- Error handling:
Result<T, E>, Option<T>
- Concurrency:
std::thread, std::sync
use std::collections::HashMap;
use std::io::{self, Write};
fn main() -> io::Result<()> {
let mut scores = HashMap::new();
scores.insert("Alice", 100);
io::stdout().write_all(b"Hello, world!\n")?;
Ok(()) // Ok(()) returns a successful Result with no value (the () is the unit type)
}
What is a HashMap?
A HashMap is a key-value data structure that allows you to store and retrieve values by their keys. Think of it like a dictionary or lookup table where each key maps to exactly one value. The “hash” part comes from the hashing function that’s used internally to quickly locate values based on their keys.
"Alice" is the key and 100 is the value. You can insert key-value pairs with insert(), retrieve values with get(), and check if a key exists with contains_key(). HashMaps are particularly useful when you need fast lookups, insertions, and deletions based on keys.
let mut scores = HashMap::new();
scores.insert("Alice", 100);
scores.insert("Bob", 85);
// Retrieve a value
// Some(value) wraps a value that exists
if let Some(score) = scores.get("Alice") {
println!("Alice's score: {}", score); // Prints: Alice's score: 100
}
// Check if key exists
if scores.contains_key("Bob") {
println!("Bob is in the map");
}
Derive Macros (#[derive])
The #[derive] attribute automatically implements common traits for your types. It’s like getting free implementations of useful functionality.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
// Now Point automatically has:
// - Debug: can be printed with {:?}
// - Clone: can be cloned with .clone()
// - PartialEq: can be compared with ==
Common derive traits:
Debug: Enables formatting with {:#?} for debugging
Clone: Allows creating copies with .clone()
Copy: Makes the type copyable (for small types like integers), although so far i’ve assumed that as default when borrowing - need to read more on this
PartialEq / Eq: Enables == and != comparisons
PartialOrd / Ord: Enables ordering (<, >, etc.)
Hash: Enables use in HashMap and HashSet keys
Default: Provides a default value
#[derive(Debug, Clone, PartialEq, Hash)]
struct Atom {
id: u32,
element: String,
coordinates: [f64; 3],
}
// All of these work automatically:
let atom1 = Atom { id: 1, element: String::from("C"), coordinates: [0.0, 0.0, 0.0] };
let atom2 = atom1.clone(); // Clone trait
println!("{:?}", atom1); // Debug trait
let mut map = HashMap::new();
map.insert(atom1, "carbon"); // Hash trait
map.insert(atom2, "carbon");
Bringing It Together
Here’s a complete example showing all these concepts:
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
struct Experiment {
id: u32,
temperature: f64,
results: Vec<f64>,
}
fn main() {
// Using std::collections::HashMap
let mut experiments = HashMap::new();
// Creating instances using :: notation
let exp1 = Experiment {
id: 1,
temperature: 298.15,
results: vec![1.0, 2.0, 3.0],
};
// Clone works because of #[derive(Clone)]
let exp2 = exp1.clone();
// Debug works because of #[derive(Debug)]
println!("Experiment: {:?}", exp1);
// PartialEq works because of #[derive(PartialEq)]
assert_eq!(exp1, exp2); // assert_eq! panics if the two values are not equal, useful for testing
experiments.insert(exp1.id, exp1);
}
Understanding these basics makes Rust code much more readable. The :: notation shows the module hierarchy, use statements clean up repetitive paths, std provides the foundation, and #[derive] gives you powerful traits without writing boilerplate.
More to come!