Work in progress
A Little Rust, Side by Side
A quick-reference Python-to-Rust cheat sheet for the project and book
Use this page as a cheat sheet
This is a quick information-retrieval page, not a chapter to memorize. When Rust syntax hides the idea, scan the headings or search for the operation, read the two columns, and return to the code.
The goal is simple: you can read all the Rust code used in the project and book. You do not need to reproduce every spelling from memory, and it is expected that you will return here when you need one.
You do not need to be good at Python. The left side is only a recognition aid: find an operation you have seen, then read across to its Rust spelling. If the Python is also new, the notice below each pair says what matters.
These are small fragments, not one long program. Later fragments reuse City, Road, and roads after they have appeared. Each fragment isolates one common Rust spelling.
Put statements in a program
city = "Logan"
fn main() {
let city = "Logan";
}
Change a binding
city = "Logan"
city = "Provo"
let mut city = "Logan";
city = "Provo";
Read a local value, a constant, and a block result
Return a value from a small computation
LIMIT = 40
def answer():
compiled = 6
return compiled + 1
value = answer()
const LIMIT: i64 = 40;
let value: i64 = {
let compiled = 6;
compiled + 1
};
Keep two values in order
road = ("Logan", "Salt Lake City")
reversed_road = ("Salt Lake City", "Logan")
road == reversed_road # False
let road = ("Logan", "Salt Lake City");
let reversed_road = ("Salt Lake City", "Logan");
road == reversed_road // false
Give the tuple shape a name
road = ("Logan", "Salt Lake City")
type City = &'static str;
type Road = (City, City);
let road: Road = ("Logan", "Salt Lake City");
Keep an ordered, growable sequence
roads = []
roads.append(("Logan", "Salt Lake City"))
roads.append(("Logan", "Provo"))
let mut roads: Vec<Road> = Vec::new();
roads.push(("Logan", "Salt Lake City"));
roads.push(("Logan", "Provo"));
Keep a set of unique roads
roads = set()
roads.add(("Logan", "Salt Lake City"))
roads.add(("Logan", "Salt Lake City"))
count = len(roads) # 1
use std::collections::HashSet;
let mut roads: HashSet<Road> = HashSet::new();
roads.insert(("Logan", "Salt Lake City"));
roads.insert(("Logan", "Salt Lake City"));
let count = roads.len(); // 1
Read a generic type
Vec<T> describes a family of sequence types. The T is a type parameter: a placeholder for the element type. Writing Vec<Road> chooses Road for that parameter. HashSet<T> and Option<T> use the same idea.
Choose the element type
city_list: list[str] = [
"Logan",
"Provo",
]
road_list: list[tuple[str, str]] = [
("Logan", "Provo"),
]
let city_list: Vec<City> = vec![
"Logan",
"Provo",
];
let road_list: Vec<Road> = vec![
("Logan", "Provo"),
];
Read a Rust name or call
| Spelling | Read it as |
|---|---|
fn f(x: T) -> U | Define f: it receives an x of type T and returns a U. |
f(x) | Call the function f. |
x.f() | Call a method on x. |
T::f() | Call a function associated with type T. |
module::T | Follow a namespace path to T. |
f::<T>(x) | Call generic f while explicitly choosing type T. |
use module::T; | Bring T into the current scope so its shorter name can be used. |
The same :: path separator appears in names such as std::collections::HashSet and syn::Expr. The ::<T> spelling is sometimes called the *turbofish*; the important fact is simply that it tells Rust which type this call should produce or use.
Test membership in a collection
Python spells membership with in. Rust collections provide methods for the same question: HashSet<T> uses contains, while iterator predicates such as any answer more general “does a matching item exist?” questions. A named membership function can wrap contains when the question belongs to the vocabulary of the program.
Test one candidate
candidate = ("Logan", "Provo")
present = candidate in roads
let candidate: Road = ("Logan", "Provo");
let present = roads.contains(&candidate);
Let a function take the set
def is_road(roads, road):
return road in roads
first = is_road(roads, ("Logan", "Provo"))
second = is_road(roads, ("Ogden", "Logan"))
fn is_road(roads: HashSet<Road>, road: Road) -> bool {
roads.contains(&road)
}
let first = is_road(roads, ("Logan", "Provo"));
let second = is_road(roads, ("Ogden", "Logan"));
// ^^^^^ value used after move
Borrow the set instead
def is_road(roads, road):
return road in roads
first = is_road(roads, ("Logan", "Provo"))
second = is_road(roads, ("Ogden", "Logan"))
fn is_road(
roads: &HashSet<Road>,
road: Road,
) -> bool {
roads.contains(&road)
}
let first = is_road(&roads, ("Logan", "Provo"));
let second = is_road(&roads, ("Ogden", "Logan"));
Separate reading from changing
before = len(roads)
answer = is_road(roads, ("Logan", "Provo"))
after = len(roads)
(before, answer, after) # (1, False, 1)
let before = roads.len();
let answer = is_road(&roads, ("Logan", "Provo"));
let after = roads.len();
(before, answer, after) // (1, false, 1)
Use a plain loop
count = 0
for _road in roads:
count += 1
let mut count = 0;
for _road in &roads {
count += 1;
}
Pass a small operation with a closure
A function can be named once and called later. A closure is an unnamed function-like value that can be stored, passed to another operation, and use names from its surroundings.
Write a lambda or closure
origin = "Logan"
destination = lambda road: road[1]
starts_here = lambda road: road[0] == origin
city = destination(("Logan", "Provo"))
answer = starts_here(("Logan", "Provo"))
let origin = "Logan";
let destination = |road: &Road| road.1;
let starts_here = |road: &Road| road.0 == origin;
let city = destination(&("Logan", "Provo"));
let answer = starts_here(&("Logan", "Provo"));
Build an iterator pipeline
Collections hold values. An iterator describes how to visit them. In Rust, iter() borrows the collection, iterator adaptors such as map and filter describe a pipeline, and a consumer such as collect, count, or any drives that pipeline.
Borrow items with iter
road_iter = iter(roads)
first = next(road_iter, None)
still_here = len(roads)
let mut road_iter = roads.iter();
let first: Option<&Road> = road_iter.next();
let still_here = roads.len();
Take ownership with into_iter
road_iter = iter(roads)
first = next(road_iter, None)
# Python still keeps roads here.
let mut road_iter = roads.into_iter();
let first: Option<Road> = road_iter.next();
// roads.len(); // error: roads was moved
Transform every item with map
destinations = list(map(
lambda road: road[1],
roads,
))
let destinations: Vec<City> = roads
.iter()
.map(|road| road.1)
.collect();
Keep matching items with filter
from_logan = list(filter(
lambda road: road[0] == "Logan",
roads,
))
let from_logan: Vec<&Road> = roads
.iter()
.filter(|road| road.0 == "Logan")
.collect();
Ask whether an item exists
has_provo = any(
road[1] == "Provo"
for road in roads
)
first_to_provo = next(
(road for road in roads
if road[1] == "Provo"),
None,
)
let has_provo = roads
.iter()
.any(|road| road.1 == "Provo");
let first_to_provo: Option<&Road> = roads
.iter()
.find(|road| road.1 == "Provo");
Reduce many items to one value
total = sum(
len(road[1])
for road in roads
)
let total: usize = roads
.iter()
.map(|road| road.1.len())
.sum();
let same_total = roads.iter().fold(
0,
|total, road| total + road.1.len(),
);
Common iterator vocabulary
| Purpose | Python spelling | Rust iterator spelling |
|---|---|---|
| Transform each item | map(f, items) | .map(f) |
| Keep matching items | filter(p, items) | .filter(p) |
| Run an action for each item | for item in items: f(item) | .for_each(f) |
| Transform and discard missing results | comprehension with a condition | .filter_map(f) |
| Produce several items from each item | nested comprehension | .flat_map(f) or .flatten() |
| Attach positions | enumerate(items) | .enumerate() |
| Pair two sequences | zip(left, right) | .zip(right) |
| Visit one sequence after another | itertools.chain(left, right) | .chain(right) |
| Ignore or limit a prefix | itertools.islice(...) | .skip(n) or .take(n) |
| Find one matching item | next((x for x in items if p(x)), None) | .find(p) |
| Test some or all items | any(...) or all(...) | .any(p) or .all(p) |
| Reduce items | sum(...) or functools.reduce(...) | .sum() or .fold(initial, f) |
| Materialize results | list(...) or set(...) | .collect::<Vec<_>>() or .collect::<HashSet<_>>() |
Use iter() when the pipeline should read borrowed items and into_iter() when it should own the items. Most adaptors are lazy; consumers such as next, collect, find, any, all, count, sum, and fold request results.
Give a value named fields with a struct
A Rust struct defines one fixed shape. Every value of that type has the same named fields, although the field values may differ.
Define one record shape
from dataclasses import dataclass
@dataclass(frozen=True)
class Atom:
relation: str
arguments: tuple[str, ...]
atom = Atom(
relation="road",
arguments=("x", "y"),
)
struct Atom {
relation: &'static str,
arguments: Vec<&'static str>,
}
let atom = Atom {
relation: "road",
arguments: vec!["x", "y"],
};
Ask for and define a default value
Rust's Default trait means “this type has a conventional starting value.” It does not invent a value for every type: a type must implement Default, and the meaning should be unsurprising for that type.
Ask for a type's default
roads: set[tuple[str, str]] = set()
limit: int = 0
maybe_road: tuple[str, str] | None = None
let roads: HashSet<Road> = Default::default();
let limit: usize = Default::default();
let maybe_road: Option<Road> = Default::default();
Use a default only when a value is missing
index: dict[str, list[tuple[str, str]]] = {}
index.setdefault("Logan", []).append(
("Logan", "Provo")
)
maybe_limit: int | None = None
limit = 0 if maybe_limit is None else maybe_limit
use std::collections::HashMap;
let mut index: HashMap<City, Vec<Road>> = HashMap::new();
index.entry("Logan").or_default().push(
("Logan", "Provo")
);
let maybe_limit: Option<usize> = None;
let limit = maybe_limit.unwrap_or_default();
Derive field-by-field defaults
from dataclasses import dataclass
@dataclass(frozen=True)
class QueryOptions:
distinct: bool = False
limit: int = 0
ordinary = QueryOptions()
limited = QueryOptions(limit=10)
#[derive(Default)]
struct QueryOptions {
distinct: bool,
limit: usize,
}
let ordinary = QueryOptions::default();
let limited = QueryOptions {
limit: 10,
..Default::default()
};
If field-by-field defaults are not the intended meaning, write an explicit impl Default instead of deriving one. If the domain has no honest conventional value, do not implement Default; require the caller to construct the value explicitly.
Recognize macro-related spellings
| Spelling | Read it as |
|---|---|
name!(...) | Invoke a function-like macro. {...} and [...] may also delimit its input. |
#[derive(X)] | Ask a derive macro to generate an implementation of trait X for the following type. |
#[proc_macro] | Register the following public function as a function-like procedural-macro entry point. |
#[derive(Default)] above is an ordinary example of the second form. The name!(...) spelling alone does not reveal whether the macro is built into Rust, defined declaratively, or implemented as a procedural macro. It identifies the invocation shape, not the implementation mechanism. #[proc_macro] registers an implementation; it does not invoke that implementation.
Represent alternatives with an enum
A data-carrying Rust enum is a tagged, disjoint union—a sum type—not a subtype hierarchy. For this syntax tree, the type has the shape
Expr = Name(String) + Call(String × List(Expr)).
The + means “one of these tagged alternatives”; the × means that the fields occur together. A constructor builds one alternative as an Expr. A pattern match reads the tag and exposes that alternative's payload.
Define the same AST union
from dataclasses import dataclass
@dataclass(frozen=True)
class Name:
text: str
@dataclass(frozen=True)
class Call:
function: str
arguments: list["Expr"]
Expr = Name | Call # a union type alias
program: list[Expr] = [
Name("x"),
Call("f", [Name("x")]),
]
enum Expr {
Name(String),
Call {
function: String,
arguments: Vec<Expr>,
},
}
let program: Vec<Expr> = vec![
Expr::Name(String::from("x")),
Expr::Call {
function: String::from("f"),
arguments: vec![
Expr::Name(String::from("x")),
],
},
];
A pattern can recognize the current form and bind names to parts of its payload in one step.
Match an AST form and bind its fields
from typing import assert_never
def child_count(expr: Expr) -> int:
match expr:
case Name(text=_):
return 0
case Call(arguments=children):
return len(children)
case _ as impossible:
assert_never(impossible)
fn child_count(expr: &Expr) -> usize {
match expr {
Expr::Name(_) => 0,
Expr::Call {
arguments: children,
..
} => {
children.len()
}
}
}
Why subtyping is the wrong model here
Declaring Name and Call as subclasses of an Expr base class describes an open family, not the closed sum above. Upcasting a node to Expr hides its alternative-specific fields. Recovering them requires runtime narrowing, a partial downcast, virtual methods, or a visitor. Adding another subclass also does not force every existing case analysis to be rechecked.
Mutable containers expose another mismatch: a list[Name] cannot safely become a list[Expr], because code holding the latter could insert a Call. Strict type checkers therefore reject that conversion. Declare the heterogeneous container at the union type from the beginning: Python uses list[Expr]; Rust uses Vec<Expr>.
For a known AST, do not introduce a Rust trait such as ExprNode and Vec<Box<dyn ExprNode>> merely to hold several node forms. Trait objects provide open polymorphism and dynamic dispatch; an enum directly represents the required closed sum.
Return to A Little Database, A Bit Rustic and use this cheat sheet whenever Rust spelling hides the database idea. Finding the spelling quickly and continuing to read is the skill this page is meant to build.