Lesson 15 / 15
Serialization
How many times a hand-written transformation body applied to a struct's fields runs per field, how that count grows multiplicatively in a nested struct, and how, in the reverse direction — from text back to a struct — one field's failure never runs the following fields' bodies at all; every body measured across the course's fifteen lessons is gathered here into a single table.
Published Updated
In this lesson
- Shows, by running it, how many times per field a transformation body applied to a struct's fields runs, and how that count grows in a nested struct
- Verifies by measurement that in text-to-struct parsing, one field's failure never runs the following fields' bodies
- Compares every body measured across the course's fifteen lessons in a single closing table, grouped by class
Contents
The Iterator Adapters lesson measured how many times the consumer at the end of a chain calls a transformation body in the source — the chain’s length stayed fixed while the number of bodies that ran varied with the consumer. This closing lesson carries the same question into a concrete data format: how many times per field does a transformation body applied to a struct’s fields run while the struct is serialized. Here, the chain’s elements are no longer a source’s entries but a single struct’s fields; the consumer, too, is singular — serialization itself. The transformation is hand-written in this lesson, with no third-party crate in use; a serialization framework would produce this body on its own, but here the body stays visible in the source and countable. The question asked throughout the course does not change here either: how many times did this body you wrote run? This is the course’s fifteenth and closing lesson; its close gathers the bodies measured across all fourteen lessons into a single table.
A Transformation Body That Runs Per Field
Had there been a serialization framework, it would have produced this transformation on its own, invisibly growing the body as the field count rose. This lesson writes the transformation by hand: every field passes through the same helper function to become a string, and this pass drops one entry into the count ledger every time. This slice of the measurement core carries only the counting ledger; no trait, bound, or error type is used in this lesson.
// times.rs — excerpt: the counting ledger.
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
pub type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;
pub fn record(ledger: &Ledger, name: &str) {
*ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}
pub fn times(ledger: &Ledger, name: &str) -> u32 {
*ledger.borrow().get(name).unwrap_or(&0)
}
A three-field Point struct carries a serialize method that turns its fields into a
string in order; every field passes through the same write_field function, and each pass
is counted. Line is a struct carrying two Point values; its own serialize does not
touch a field of its own, it calls the serialize of the two Point values it carries.
Triangle is a struct carrying three Line values; the same pattern goes one level deeper.
// field_body.rs — measures how many times a body runs per field, shows multiplicative growth in nesting.
mod times;
use times::{times, record, Ledger};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
fn write_field(ledger: &Ledger, name: &str, value: i32) -> String {
record(ledger, "field-body");
format!("{}={}", name, value)
}
struct Point {
x: i32,
y: i32,
z: i32,
}
impl Point {
fn serialize(&self, ledger: &Ledger) -> String {
let x = write_field(ledger, "x", self.x);
let y = write_field(ledger, "y", self.y);
let z = write_field(ledger, "z", self.z);
format!("{};{};{}", x, y, z)
}
}
struct Line {
start: Point,
end: Point,
}
impl Line {
fn serialize(&self, ledger: &Ledger) -> String {
format!("{}|{}", self.start.serialize(ledger), self.end.serialize(ledger))
}
}
struct Triangle {
edge1: Line,
edge2: Line,
edge3: Line,
}
impl Triangle {
fn serialize(&self, ledger: &Ledger) -> String {
format!(
"{}#{}#{}",
self.edge1.serialize(ledger),
self.edge2.serialize(ledger),
self.edge3.serialize(ledger)
)
}
}
fn main() {
let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
let before = times(&ledger, "field-body");
let point = Point { x: 1, y: 2, z: 3 };
let _ = point.serialize(&ledger);
let after = times(&ledger, "field-body");
println!("Point (3 fields): {} body runs", after - before);
let before = after;
let line = Line {
start: Point { x: 0, y: 0, z: 0 },
end: Point { x: 5, y: 5, z: 5 },
};
let _ = line.serialize(&ledger);
let after = times(&ledger, "field-body");
println!("Line (2 points, 6 fields): {} body runs", after - before);
let before = after;
let triangle = Triangle {
edge1: Line {
start: Point { x: 0, y: 0, z: 0 },
end: Point { x: 1, y: 1, z: 1 },
},
edge2: Line {
start: Point { x: 1, y: 1, z: 1 },
end: Point { x: 2, y: 2, z: 2 },
},
edge3: Line {
start: Point { x: 2, y: 2, z: 2 },
end: Point { x: 0, y: 0, z: 0 },
},
};
let _ = triangle.serialize(&ledger);
let after = times(&ledger, "field-body");
println!("Triangle (3 lines, 18 fields): {} body runs", after - before);
println!("total field-body runs: {}", times(&ledger, "field-body"));
}
Point (3 fields): 3 body runs Line (2 points, 6 fields): 6 body runs Triangle (3 lines, 18 fields): 18 body runs total field-body runs: 27
CI11. Point carries three fields, and its serialize drops exactly three
field-body entries — one to one. Line does not touch a field of its own at all, it only
calls the serialize of its two Point values; but since each call carries its own three
entries, the total comes to six. Triangle carries the same pattern one level deeper: each
of its three Line values leaves six entries, for a total of eighteen. The relationship
between field count and body run count is one to one here — but this relationship only
holds for the leaf-level actual fields (x, y, z); Line and Triangle’s own bodies
never leave a field-body entry themselves, they only delegate to the sub-structures they
carry. Nesting grows the count multiplicatively rather than additively: Triangle’s three
edges each multiply their six entries.
Parsing in Reverse: One Field’s Failure Stops the Rest
The same question can also be asked in reverse: how many bodies run per field to turn a
string back into a struct? Parsing, too, is written by hand — every field passes through a
shared read_field function; this function records first, then tries to parse the piece of
the string. Point::deserialize reads its three fields in order and places a question mark
after each read call — the early return measured in the Shorthand for Error Propagation
lesson holds here as well: if one field fails, the question mark exits the function without
ever running the remaining fields’ bodies.
// parsing.rs — reverse direction: text to struct, body runs per field.
mod times;
use times::{times, record, Ledger};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
struct ParseError;
struct Point {
x: i32,
y: i32,
z: i32,
}
fn read_field(ledger: &Ledger, name: &str, key: &str, part: &str) -> Result<i32, ParseError> {
record(ledger, key);
let pair: Vec<&str> = part.splitn(2, '=').collect();
if pair.len() != 2 || pair[0] != name {
return Err(ParseError);
}
pair[1].parse::<i32>().map_err(|_| ParseError)
}
impl Point {
fn deserialize(ledger: &Ledger, prefix: &str, text: &str) -> Result<Point, ParseError> {
let parts: Vec<&str> = text.split(';').collect();
if parts.len() != 3 {
return Err(ParseError);
}
let x = read_field(ledger, "x", &format!("{}-x", prefix), parts[0])?;
let y = read_field(ledger, "y", &format!("{}-y", prefix), parts[1])?;
let z = read_field(ledger, "z", &format!("{}-z", prefix), parts[2])?;
Ok(Point { x, y, z })
}
}
fn main() {
let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
let valid = Point::deserialize(&ledger, "valid", "x=1;y=2;z=3");
if let Ok(p) = &valid {
println!("valid text parsed: x={} y={} z={}", p.x, p.y, p.z);
}
println!(
"valid-x: {} valid-y: {} valid-z: {}",
times(&ledger, "valid-x"),
times(&ledger, "valid-y"),
times(&ledger, "valid-z")
);
let invalid = Point::deserialize(&ledger, "invalid", "x=1;y=not;z=3");
println!("did invalid text succeed: {}", invalid.is_ok());
println!(
"invalid-x: {} invalid-y: {} invalid-z: {}",
times(&ledger, "invalid-x"),
times(&ledger, "invalid-y"),
times(&ledger, "invalid-z")
);
}
valid text parsed: x=1 y=2 z=3 valid-x: 1 valid-y: 1 valid-z: 1 did invalid text succeed: false invalid-x: 1 invalid-y: 1 invalid-z: 0
CI12. For the valid string (x=1;y=2;z=3), all three fields are read and three separate
counters each go up by one — the forward direction’s one-to-one relationship holds here too.
For the invalid string (x=1;y=not;z=3), the x field is read and succeeds, and the y
field is also read but fails to parse as a number — both of their counters go up by one,
because both were at least attempted. The z field’s counter stays at zero: the question
mark exits the function the moment it catches y‘s failure, and the body written for z is
never touched. This shows that the forward direction’s every time class does not stay
unconditional in reverse: a field’s body becomes dependent on the fields before it
succeeding.
A Bounding Measurement
The one-to-one relationship between field count and body run count holds in this lesson only because every struct used here carries unconditional fields — every field always exists, always gets transformed. Had the type of absence measured in the Expressing Absence with a Type lesson been a field itself, that field’s transformation body would no longer run unconditionally: it would run for some values and not for others, and counting the fields alone would no longer give the body run count. This lesson does not run that case, it only states under which condition the relationship holds and under which it breaks.
A second boundary concerns what the count does not measure. The field-body counter only
counts how many times the body was called — it does not measure how long the string it
produced is, or how expensive one field’s transformation was. A single integer field and a
field carrying a large collection are the same to this counter — both are counted once. This
distinction, carried since the start of the course, holds here too: the number only shows
how many times something ran, not how long it took or what it produced.
Closing Table
| Lesson | Body measured | Class | Bounding measurement |
|---|---|---|---|
| Trait Definition and Implementation | Default trait body (name) |
every time · never | Zero runs does not mean the body is absent from the source |
| Trait Bounds | Run count of the bounded and unbounded body | every time | The cost is at compile time: a separate body per type, unmeasurable by running |
| Associated Types | Associated-type / generic-parameter calls | every time | The difference is not in the run but in what the caller must write |
| Object Safety and Dynamic Dispatch | static-dispatch / dynamic-dispatch |
every time | Same count is not the same cost; how the call is resolved is not measurable |
| Derived Traits | Hand-written / derived equality body | every time (hand-written) · uncountable (derived) | The derived body is not written in the source, its run count cannot be counted |
| Operator Overloading | operator-body (add) |
every time | The call site is invisible in the source; the count is only known from the ledger |
| Expressing Absence with a Type | Eager / lazy default body | every time · only when needed · never | If the body is cheap the two forms give the same result; only the count differs |
| Failure as a Return Value | Success / failure branch | only when needed · never | Covered code and executed code are separate facts |
| The Shorthand for Error Propagation | Body after the question mark | only when needed · never | Early return silently skips the rest; the side effect also never happens |
| Custom Error Types | Error conversion body (From / map_err) |
every time | The implicit conversion cannot access the caller’s context; the loss is irreversible |
| Panic and Unrecoverable Errors | Remaining body (recoverable/unrecoverable) | every time · never | Panic is a design decision, not an error type; no panic text was printed |
| Closures | Uncalled closure body | never | A closure’s own definition is separate work that never enters the ledger |
| The Iterator Trait | Unconsumed chain’s body | never | An unconsumed chain is not free; its cost cannot be measured by the run counter |
| Iterator Adapters | Consumed / short-circuiting chain body | every time · only when needed | Order is not read from the source, only observed with a trace |
| Serialization | Per-field transformation body | every time · never | The relationship holds only when every field is unconditional; a conditional field changes the class |
Summary
- A transformation body applied to a struct’s fields runs once per field; in a struct with
n fields, this body runs n times — three for
Point, six forLine(which carries twoPoints), eighteen forTriangle(which carries threeLines). - In a nested struct, field count grows multiplicatively (3, 6, 18), and the body’s run count tracks this growth exactly.
- This body falls into the every time class: every field is inevitably transformed every time serialization is called.
- In the reverse direction — text to struct — when one field’s parsing fails, the question mark silently skips the remaining fields’ body; these fields never run in that run.
- If a field is not unconditional (if it carries a type of absence), this relationship breaks; counting fields no longer gives the body run count.
- Across fifteen lessons, every body measured settled into three classes (every time, only when needed, never); the closing table gathers them in one place.
Course Wrap-Up
Across fifteen lessons, this course applied a single question to one mechanism after another: how many times did this body you wrote run? The Traits and Generics topic asked this question of a behavior separating from its type, of whether a bound adds anything to the run, of a contract’s two different forms, of whether a dispatch form is resolved at compile time or at run time, of a body being able to run without ever being written in the source at all, and of a call hidden behind an operator. The Error Handling topic carried the same question to a fallback value, a success/failure branch, the branching behind a shorthand, an error conversion, and an unrecoverable state. Closures and Iterators, and this closing lesson, applied the same question to a definition itself, a traversal contract, a consumer at the end of a chain, and now to a struct’s fields. Of the sixteen bodies in the measurement core, eight ran every time, three ran only when needed, and five never ran — this number did not change across the course, only which mechanism fell into which class changed.
The Concurrency and Asynchronous Rust course does not drop this question, it changes its subject: instead of how many times a body ran, it will ask what a task needs in order to make progress — whether it advances on its own, is advanced by a poller, waits for something else to finish, or does not advance at all. Next to the Object-Oriented Java course’s question of which type made the decision, and the Interfaces, Errors and Generics course’s question of where the bond forms, this course added the question of how many times something ran; the next course will add to this trio the question of what is needed in order to make progress.
To keep your progress and take notes, Log in
My notes
Log in to take notes.