Skip to content
academia.sh

Lesson 05 / 15

Derived Traits

An attribute line can implement a trait without hand-writing it — derive; a hand-written body can place a counter call into the run ledger, while a derived body has no source line to count it in, so it can only be observed by its behavior — and the same derive gives two different results on two different data layouts.

Published Updated

In this lesson

  • Shows that the derive attribute implements a trait without hand-writing it
  • Compares a hand-written body's run count being countable against a derived body's not being countable, and explains why
  • Demonstrates by running it that the same derive can give different results on different data layouts
Contents

The previous four lessons measured hand-written bodies: a trait’s default body, a bounded generic body, a body carrying an associated type, a body called statically or dynamically — all of them stood somewhere in the source, under an fn line. A trait implementation is sometimes never hand-written at all: the #[derive(...)] attribute line tells the compiler “you generate this trait for this type,” and produces the body without it ever being written in the source. This lesson asks how many times a body of this kind runs — and shows that this time the question cannot be answered with an exact number.

The Difference Between Hand-Written and Derived

An equality comparison for a type can be built two ways: by hand-writing the impl PartialEq for Type block, or by placing the #[derive(PartialEq)] line above the type. Both produce the same result — they say whether two values are equal field by field — but their counterpart in the source is different.

// times.rs — slice: the record/times ledger, a hand-written equality body.

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)
}

// Hand-written implementation: the body sits in the source, it can be counted.
pub struct ManualEqual {
    pub x: i32,
    pub y: i32,
    pub ledger: Ledger,
}

impl PartialEq for ManualEqual {
    fn eq(&self, other: &Self) -> bool {
        record(&self.ledger, "manual-equality-body");
        self.x == other.x && self.y == other.y
    }
}

// Derived implementation: the body is not in the source, the compiler generates a field-by-field comparison.
#[derive(PartialEq)]
pub struct DerivedEqual {
    pub x: i32,
    pub y: i32,
}

ManualEqual’s eq method carries a two-line body in the source: first a record call, then a field-by-field comparison. DerivedEqual carries no impl PartialEq block at all — its only line is #[derive(PartialEq)], and this line is not a method body, it is an instruction to the compiler. When the compiler reads this instruction, it generates its own eq body; this body does the same job as ManualEqual’s (comparing the x fields and the y fields), but it never appears in any line of the source.

A single #[derive(...)] line can produce more than one trait at once; trait names separated by commas are written inside the parentheses. Clone, already seen in this course, can be derived this way too, and so can Debug (a trait that turns a value into a readable text form):

// derive-debug.rs — a Debug derive turns fields into a text form automatically.
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let a = Point { x: 3, y: 5 };
    let b = a.clone();
    println!("{:?}", a);
    println!("is_equal={}", a == b);
}
Point { x: 3, y: 5 }
is_equal=true

All three traits (Debug, Clone, PartialEq) were derived on a single line, and none of the three has a body visible in the source. The {:?} output shows that Debug’s generated body gathers up the field names and values on its own — this pattern was not hand-written either, it was generated by the compiler, just like eq. These three traits can be derived together because all three rest on the same principle: apply the same operation (printing, cloning, comparing) to every field of the type, in order. This same shared principle will come up again in a later lesson (serialization).

Countable and Uncountable

// derive-uncountable.rs — a hand-written body can be counted, a derived body cannot.
mod times;
use times::{times, Ledger, ManualEqual, DerivedEqual};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let a = ManualEqual { x: 1, y: 2, ledger: Rc::clone(&ledger) };
    let b = ManualEqual { x: 1, y: 2, ledger: Rc::clone(&ledger) };
    let c = ManualEqual { x: 9, y: 9, ledger: Rc::clone(&ledger) };

    let is_equal = a == b;
    let is_not_equal = a == c;
    println!("manual: is_equal={} is_not_equal={}", is_equal, is_not_equal);
    println!("manual-equality-body={}", times(&ledger, "manual-equality-body"));

    // The derived PartialEq's body is not written in the source - so we cannot put
    // a record call inside it. We can only observe its behavior.
    let p = DerivedEqual { x: 1, y: 2 };
    let q = DerivedEqual { x: 1, y: 2 };
    let r = DerivedEqual { x: 9, y: 9 };
    println!("derived: is_equal={} is_not_equal={}", p == q, p == r);
}
manual: is_equal=true is_not_equal=false
manual-equality-body=2
derived: is_equal=true is_not_equal=false

TG10. manual-equality-body ran twice — both the a == b and a == c calls triggered the eq body, because a record call was hand-placed inside the body. This row falls into the every time class: the == operator runs unavoidably every time it is used. We cannot write the same row for DerivedEqual — since we cannot add a single source line inside the body the compiler generates, we cannot place a counter to show how many times it ran. We can see that both the p == q and p == r calls give the correct result (a behavioral observation), but we cannot write the count of how many times the body behind those two calls was triggered into the run ledger. This shows that a body with no line written in the source cannot be placed with certainty into any of the course’s three classes — that it runs is certain (a call always produces a result), but its run count cannot be measured with this method.

The Object-Oriented Java course showed that a record type’s equality and text representation are built automatically — what was measured there was which type made the decision, not whether the record’s own generated body runs. This lesson looks at a similar automatic generation, but asks a different question: can this generated body be counted by the course’s counting-based method? The answer is no — the lesson says so openly, it does not make up a number.

A derived body being uncountable does not mean it is random or unpredictable — quite the opposite, what #[derive(PartialEq)] will produce is fully deterministic: it compares the type’s fields one by one in declaration order, true if all are equal, false if even one is not. This rule is the same for every #[derive(PartialEq)], and can be stated in advance just by looking at the type declaration, without ever running it. What cannot be counted is not the behavior itself, it is that behavior’s run-time call count: to see how many times eq runs, we need to add a line to the run ledger, and there is no line to add it to in derived source.

“The body is not in the source” does not mean “no body compiles.” The #[derive(...)] line generates, at compile time, a piece of code resembling an impl block, and this generated code compiles the same way a hand-written impl block would — the generation happens in a separate stage, before the rest of compilation. In the source the author sees, this generated code never appears at all — only the #[derive(PartialEq)] line is visible, the dozens of lines of comparison logic it produces are not. What this lesson means by “an unwritten body still runs” is exactly this: something runs, it genuinely executes, but it has no counterpart in the source the author reads. This is also why we cannot add a counter call — adding a counter needs a source line to add it onto, and there is no such line in derived source.

Same Derive, Different Data Layout, Different Result

A derive having no body in the source might give the impression that it means the same thing for every type. It does not: the same #[derive(Clone)] line produces two different behaviors on two different data layouts. derive always says “apply the same operation field by field,” but what “the same operation” means depends on the field’s own type — cloning an i32 means writing a new number, cloning an Rc<...> means incrementing a counter and sharing the same cell.

// derive-layout.rs — the same derive gives two different results on two data layouts.
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Clone)]
struct Linked {
    value: Rc<RefCell<i32>>,
}

#[derive(Clone)]
struct Separate {
    value: i32,
}

fn main() {
    let a = Linked { value: Rc::new(RefCell::new(1)) };
    let b = a.clone();
    *a.value.borrow_mut() += 41;
    println!("does the linked clone share: {}", *b.value.borrow() == 42);

    let c = Separate { value: 1 };
    let mut d = c.clone();
    d.value += 41;
    println!("does the separate clone share: {}", c.value == d.value);
}
does the linked clone share: true
does the separate clone share: false

TG11. Linked‘s clone shares the value in the source: a change made through a is also seen in b, because the derived clone of an Rc<RefCell<i32>> field just increments the counter and hands back a second reference to the same cell — the behavior built in the Ownership and the Memory Model course’s Reference Counting lesson. Separate’s clone, on the other hand, is independent: a change made on d never touches c at all, because the derived clone of an i32 field produces an independent copy on its own. The #[derive(Clone)] line is exactly identical on both, but the result diverges by data layout — derive is a shortcut for saying “clone field by field,” it does not concern itself with what the field itself means.

This distinction shows once more the difference between a hand-written body and a derived one: a hand-written impl Clone could, if it wanted, be written to produce an independent copy for Linked too — by reading the value inside the RefCell and building a new Rc. A derived body does not offer this choice; it always says “clone the fields as they are,” inheriting whatever the field’s own Clone implementation does. Whether a type’s clone will be shared or independent is decided not by the derive but by the choice of field types.

Derive also has a quiet precondition: writing #[derive(PartialEq)] for a struct requires that every field of the struct itself implements PartialEq — the compiler calls == for every field while generating the field-by-field comparison, and this call needs to compile, which requires the field’s own type to be comparable. Point‘s x and y fields being i32, and i32 already carrying PartialEq, is why the derive compiles without a problem. If one of the fields carried an incomparable type (a function pointer, say), the same #[derive(PartialEq)] line would be rejected by the compiler — this rejection, too, by the course’s general rule, is not put in a code block; the Point and ManualEqual examples above already show the accepted counterpart: structs carrying only comparable fields.

The Measurement’s Limit

Derive does not mean the same thing on every type. The same #[derive(Clone)] can produce a shared copy on one type and an independent copy on another; the same #[derive(PartialEq)] can compare two fields on one type and ten fields on another — what the generated body does depends on the fields of the type it is built on. This lesson does not count how many times a derived body ran, and it does not assume that two different derives do the same job either; it only shows that both are generated by the compiler and that their behavior can be observed by running them. The course’s measurement method hits a clear limit here: wherever a counter call cannot be written in the source, behavior comparison takes over instead of a number.

This limit has a practical consequence: trusting a derived body requires understanding that body’s design, because there is no way to trace it at run time. Writing #[derive(Clone)] for Linked‘s Rc<RefCell<i32>> field makes the decision “let this type’s clone be shared“ invisibly — this decision is not visible to someone reading the type declaration from the derive line, it is visible from the field’s own type. A hand-written impl Clone could show this decision openly inside the body (is a reference being cloned, or a value); a derived body hands this decision off to the field type and does not write it in the source separately. This is the cost of derive’s shortcut: the source gets shorter, but whoever reads the source has to go down to the field types to understand the behavior.

The previous four lessons could measure the run count directly with a record call, because the body always stood somewhere under an fn line in the source. This lesson breaks that assumption: the body is not in the source, but the behavior is, and it can be verified. The course’s measurement method chooses to stay silent where it cannot count — instead of making up a number, it says “there is no number here.”

Summary

  • The derive attribute (#[derive(...)]) implements a trait without hand-writing it, by having the compiler generate it; it leaves no method body in the source.
  • A hand-written body (manual-equality-body) falls into the every-time class and can be counted — it ran twice in this lesson; a derived body runs every time it is called too, but since it has no body in the source, this run cannot be counted.
  • An uncountable body’s behavior can still be observed by running it: whether it gives the correct result can be seen, how many times it ran cannot.
  • The same derive can give two different results on two different data layouts — it produces a shared copy on a type with an Rc<RefCell<...>> field, an independent copy on a plain numeric field.

Next Step

This lesson showed that a body runs even when it is never written at all. The course’s last lesson looks at another kind of invisibility: where an operator (like +) is called never appears in the source, only the operator itself is visible. The next lesson counts how many times this invisible call runs by running a three-term sum.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close