4. Process a list of orders
The task
Section titled “The task”Turn a list of orders into a list of invoices, with the order ID, the customer, and the total.
The versions
Section titled “The versions”WorseClones the whole list, then clones the lines and the customer of each order again.
pub fn make_invoices(orders: &Vec<Order>) -> Vec<Invoice> {let mut invoices = Vec::new();for order in orders.clone() {let lines = order.lines.clone();let total_cents = lines.iter().sum();invoices.push(Invoice {order_id: order.id,customer: order.customer.clone(),total_cents,});}invoices}OKBorrows the orders. Clones only the customer name, because the invoice owns it.
pub fn make_invoices(orders: &[Order]) -> Vec<Invoice> {orders.iter().map(|order| Invoice {order_id: order.id,customer: order.customer.clone(), // the invoice owns its own copytotal_cents: order.lines.iter().sum(),}).collect()}BetterTakes the orders by value and moves each customer name into its invoice. No copies at all.
pub fn into_invoices(orders: Vec<Order>) -> Vec<Invoice> {orders.into_iter().map(|order| Invoice {order_id: order.id,total_cents: order.lines.iter().sum(),customer: order.customer, // moved, not copied}).collect()}
The “Worse” version has three copies for each order: orders.clone() copies every order, order.lines.clone() copies the lines again, and order.customer.clone() copies the name again. Copies like these usually come from borrow errors during development. The agent tried to move order.customer out of a borrowed order, got “cannot move out of order.customer, which is behind a shared reference” (E0507), and added .clone() calls until the errors stopped. See S01: clone() to satisfy the borrow checker.
The difference between “OK” and “Better” is a question of ownership, not of style:
- “OK” borrows (
&[Order]). The caller keeps its orders. The invoices need their own customer names, so the function clones oneStringfor each invoice. That clone has a reason. - “Better” consumes (
Vec<Order>). The caller gives the orders away.into_iter()gives each order by value, soorder.customercan move into the invoice. The testbetter_reuses_the_customer_stringsproves that the invoice uses the same heap buffer as the order.
Verdict
Section titled “Verdict”| Criterion | OK | Better |
|---|---|---|
| Correctness | Same | Same |
| All three give the same invoices. | ||
| Failure behavior | Same | Same |
| Nothing can fail. | ||
| Performance | Better | Better |
| "Worse" copies every order twice. "OK" copies one string for each order. "Better" copies nothing. | ||
| Readability | Better | Better |
An iterator chain with one map. Each remaining clone has a reason. | ||
| API ergonomics | Better | Same |
"OK" accepts any slice. "Better" needs an owned Vec, so it fits only callers that are finished with their orders. | ||
Trade-offs
Section titled “Trade-offs”- “OK” is the right default. Most callers still need their orders after they make the invoices: to save them, to log them, or to show them. A function that borrows works for every caller.
- “Better” is right at the end of a pipeline, where the orders are not needed again: for example, the last step of an import job. Name it so that the move is clear (
into_invoices), as the standard library does withinto_iterandinto_bytes. - Offer both if callers need both.
into_invoices(orders)andmake_invoices(&orders)next to each other is a common pattern. - “Worse” is never better than “OK”. The copies do nothing that borrowing does not do.