Skip to content
academia.sh

Lesson 01 / 15

Trait Definition and Implementation

A trait establishes a behavior contract independent of type and can carry a default method body; when a sixteen-body measurement kernel runs, this default body is seen to run four times for one type and zero times for a type that overrides it — the question asked throughout the course is this: how many times did this body you wrote in the source run?

Published Updated

In this lesson

  • Shows that a trait definition establishes a behavior contract and can carry a default method body
  • Verifies by running the measurement kernel that a default body runs every time for one type and never for a type that overrides it
  • Distinguishes the trait concept from the interface concept and explains the difference in measurement axis with Interfaces, Errors and Generics
Contents

The Ownership and the Memory Model course measured where a value’s life ends: at the end of its scope, where it was moved, in an explicit call, or together with its container. While a value stays in hand for the length of its life, what is done with it is a separate question, and this course looks there. Separating a behavior from a type — introducing the same function under the same name to more than one type — turns into a decision that also determines how many times that behavior’s body runs. One question is asked throughout the course: how many times did this body you wrote in the source run?

There are three answers to this question, and these three names do not change throughout the course.

Three Classes

Class Definition
every time Runs unavoidably, per call or per element
only when needed Runs only when the result is requested, as much as needed
never Written in the source, but did not run even once in this run

This trio should not be confused with the two questions already asked earlier. The Object-Oriented Java course measured which type made the decision — the declared type or the runtime type. The Interfaces, Errors and Generics course (Go) measured where the binding was established — at declaration, at the call site, or inside the value. This course asks neither; it asks counting. By the time a trait implementation is written, the binding is already established — what’s measured in this lesson is how many times that body was triggered at run time, after the binding was established.

A trait is a behavior contract: it declares one or more method signatures and lets those methods be called under a type-independent name. The function definition in the Programming Fundamentals course was bound to a single type; a trait generalizes this — the same method name can correspond to a separate body for every type that implements it. This looks close to the interface concept, but it is not the same thing: an interface was defined in the Go course on the axis of where the binding is established (implicit implementation, without requiring a declaration); a trait, in Rust, is a contract that a type declares explicitly with the impl Name for Type syntax, and it can also carry a default body. The word trait will be used untranslated throughout the course.

Required and Optional Methods

A trait declaration lists a series of method signatures inside the body of trait Name { ... }. Each signature can take one of two forms: only a header (fn name(&self) -> Type;, ending in a semicolon, without a body) or a header plus a body (fn name(&self) -> Type { ... }). The first form is required: every type that implements the trait must write this method in its own impl block, otherwise the implementation is considered incomplete and the source does not compile. The second form is optional: an implementing type may never write this method — if it does not, the trait’s own body takes over; if it does, its own body runs and the trait’s is never seen.

The Label trait used throughout this lesson shows all three at once. ledger and kind are declared without a body — every implementation has to write these two itself, because the trait itself cannot assume anything about a type’s ledger or name. name, on the other hand, is declared with a default body: Plain never writes this method and inherits the trait’s body, while Custom writes its own body and overrides the default. Required and optional methods can coexist inside the same trait declaration; this is similar to a contract saying that some of its clauses must differ for every signer (address, name) while others can start with a shared default and be changed only when needed.

The impl Label for Plain syntax itself splits into two parts as well: Label says which contract is being implemented; Plain says who implements it. The same trait can be implemented repeatedly for different types with separate impl blocks — times.rs has two separate impl Label for ... blocks for Plain and Custom, both carrying the same trait name but implementing independent bodies.

The Measurement Kernel

The kernel from which the rest of the course will take slices is times.rs. It classifies sixteen separate bodies in a single run and records each one to an observation ledger. The ledger reuses the mechanism built in the previous course’s shared-ownership lesson: Rc<RefCell<BTreeMap<String, u32>>> — a counter with more than one owner, carrying interior mutability, and sorted.

// times.rs — how many times did a body run: every time, only when needed, or never.

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::ops::Add;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

// --- trait: default body and the type that overrides it ---

trait Label {
    fn ledger(&self) -> &Ledger;
    fn kind(&self) -> &'static str;
    fn name(&self) -> String {
        let key = format!("default-body-{}", self.kind());
        record(self.ledger(), &key);
        String::from("default")
    }
}

struct Plain {
    ledger: Ledger,
}
struct Custom {
    ledger: Ledger,
}

impl Label for Plain {
    fn ledger(&self) -> &Ledger {
        &self.ledger
    }
    fn kind(&self) -> &'static str {
        "plain"
    }
}

impl Label for Custom {
    fn ledger(&self) -> &Ledger {
        &self.ledger
    }
    fn kind(&self) -> &'static str {
        "custom"
    }
    fn name(&self) -> String {
        record(self.ledger(), "overridden-body");
        String::from("custom")
    }
}

// --- generics vs. trait object: the same job, two call forms ---

fn static_dispatch<T: Label>(x: &T) -> String {
    record(x.ledger(), "static-dispatch");
    x.name()
}

fn dynamic_dispatch(x: &dyn Label) -> String {
    record(x.ledger(), "dynamic-dispatch");
    x.name()
}

// --- operator overloading ---

#[derive(Clone)]
struct Length {
    value: i32,
    ledger: Ledger,
}

impl Add for Length {
    type Output = Length;
    fn add(self, other: Length) -> Length {
        record(&self.ledger, "operator-body");
        Length {
            value: self.value + other.value,
            ledger: Rc::clone(&self.ledger),
        }
    }
}

// --- the error path ---

#[derive(Debug)]
struct NumberError;

impl From<std::num::ParseIntError> for NumberError {
    fn from(_: std::num::ParseIntError) -> NumberError {
        NumberError
    }
}

fn parse_doubled(ledger: &Ledger, text: &str, label: &str) -> Result<i32, NumberError> {
    let n: i32 = text.parse()?;
    record(ledger, label);
    Ok(n * 2)
}

fn eager_default(ledger: &Ledger) -> i32 {
    record(ledger, "eager-default");
    -1
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let mut classified: Vec<(&str, &str)> = Vec::new();

    // 1-2. default body: runs for the type that does not override it, never for the one that does
    let plain = Plain {
        ledger: Rc::clone(&ledger),
    };
    let custom = Custom {
        ledger: Rc::clone(&ledger),
    };
    let _ = plain.name();
    let _ = custom.name();
    let _ = custom.name();

    // 3-4. the same job in two call forms
    let _ = static_dispatch(&plain);
    let _ = static_dispatch(&plain);
    let _ = dynamic_dispatch(&plain as &dyn Label);
    let _ = dynamic_dispatch(&custom as &dyn Label);

    // 5. operator body
    let a = Length {
        value: 3,
        ledger: Rc::clone(&ledger),
    };
    let b = Length {
        value: 4,
        ledger: Rc::clone(&ledger),
    };
    let sum = a.clone() + b;
    let sum = sum + a;
    let _ = sum.value;

    // 6. eager default: runs even when a value IS present
    let full: Option<i32> = Some(7);
    let _ = full.unwrap_or(eager_default(&ledger));

    // 7-8. lazy default: never when a value is present, once when it is absent
    let empty: Option<i32> = None;
    let _ = full.unwrap_or_else(|| {
        record(&ledger, "lazy-default-unneeded");
        -1
    });
    let _ = empty.unwrap_or_else(|| {
        record(&ledger, "lazy-default-needed");
        -1
    });

    // 9-10. after the question mark operator
    let _ = parse_doubled(&ledger, "21", "post-question-succeeded");
    let _ = parse_doubled(&ledger, "twenty-one", "post-question-failed");

    // 11. error conversion
    if parse_doubled(&ledger, "twenty-one", "ignored").is_err() {
        record(&ledger, "error-conversion");
    }

    // 12. unconsumed chain
    let _lazy = vec![1, 2, 3, 4].into_iter().map(|x| {
        record(&ledger, "unconsumed-chain");
        x * 2
    });

    // 13. consumed chain: on every element
    let sum: i32 = vec![1, 2, 3, 4]
        .into_iter()
        .map(|x| {
            record(&ledger, "consumed-chain");
            x * 2
        })
        .sum();
    let _ = sum;

    // 14. short-circuiting chain: only as much as needed
    let found = vec![1, 2, 3, 4]
        .into_iter()
        .map(|x| {
            record(&ledger, "short-circuiting-chain");
            x * 2
        })
        .find(|x| *x > 2);
    let _ = found;

    // 15. uncalled closure
    let _closure = || record(&ledger, "uncalled-closure");

    // --- report ---
    let bodies = [
        ("default-body-plain", "every time"),
        ("overridden-body", "every time"),
        ("static-dispatch", "every time"),
        ("dynamic-dispatch", "every time"),
        ("operator-body", "every time"),
        ("eager-default", "every time"),
        ("consumed-chain", "every time"),
        ("error-conversion", "every time"),
        ("lazy-default-needed", "only when needed"),
        ("post-question-succeeded", "only when needed"),
        ("short-circuiting-chain", "only when needed"),
        ("default-body-custom", "never"),
        ("lazy-default-unneeded", "never"),
        ("post-question-failed", "never"),
        ("unconsumed-chain", "never"),
        ("uncalled-closure", "never"),
    ];

    println!("{:<28}{:<22}{}", "body", "class", "how many times");
    for (name, class) in bodies {
        let n = times(&ledger, name);
        println!("{:<28}{:<22}{}", name, class, n);
        classified.push((name, class));
    }

    println!();
    for s in ["every time", "only when needed", "never"] {
        let count = classified.iter().filter(|(_, c)| *c == s).count();
        println!("{:<22}{}", s, count);
    }
    println!("total bodies: {}", classified.len());
}
body                        class                 how many times
default-body-plain          every time            4
overridden-body             every time            3
static-dispatch             every time            2
dynamic-dispatch            every time            2
operator-body               every time            2
eager-default               every time            1
consumed-chain              every time            4
error-conversion            every time            1
lazy-default-needed         only when needed      1
post-question-succeeded     only when needed      1
short-circuiting-chain      only when needed      2
default-body-custom         never                 0
lazy-default-unneeded       never                 0
post-question-failed        never                 0
unconsumed-chain            never                 0
uncalled-closure            never                 0

every time            8
only when needed      3
never                 5
total bodies: 16

TG1. This run is deterministic: it was verified across five separate runs that the source’s same sixteen lines are produced in the same order with the same class distribution. 8 of the sixteen bodies ran every time, 3 ran only when needed, 5 never ran. This number does not change through the rest of the course; each lesson only takes its own slice. The report uses a sorted map (BTreeMap) — because a hash map’s iteration order could change from run to run, the count here relies on a structure that guarantees ordering even for this.

A Default Body Gives Two Different Results for Two Types

The Label trait declares three methods: ledger and kind carry no body at all — every implementation has to write these itself. The third method, name, carries a default body: since impl Label for Plain never touches this body, the trait’s own body runs when name is called on Plain. Custom, on the other hand, overrides name with its own body — the default body in the trait never takes over for Custom.

The two rows in the table show this pair directly. default-body-plain ran four times: plain.name() was called directly once, static_dispatch(&plain) was called twice and each call triggered x.name() inside itself, and dynamic_dispatch(&plain ...) was called once and it triggered x.name() the same way — four calls in total, all four landing on the trait’s default body. overridden-body ran three times: custom.name() twice, directly, and dynamic_dispatch(&custom ...) once — all three ran Custom‘s own body, never touching the trait’s default. Both belong to the every time class: they run unavoidably wherever they are called, the only difference being which body runs.

TG2. The Interfaces, Errors and Generics course measured where the binding was established for an implementation — in Go, a type implicitly implemented an interface without any declaration. In Rust this binding is established explicitly with the impl Label for Plain line; this course notes that difference once and moves on. What’s actually measured here is how many times that implementation’s body is triggered at run time after the binding is established — the difference of 4 vs. 3 between default-body-plain and overridden-body comes from the number of calls, not from the binding itself.

eager-default, operator-body, and error-conversion on the error path fall into the same class too — they run every time, because each one in the kernel runs exactly as many times as it is called. The course’s later lessons will each grow one of these rows in turn: Trait Bounds will grow whether static-dispatch depends on a bound, Associated Types will grow the Output field in operator-body’s signature, Object Safety and Dynamic Dispatch will put static-dispatch and dynamic-dispatch side by side.

The Measurement’s Limit

The default-body-custom row is zero. This does not mean the default body does not exist — it sits in the source, inside the trait Label declaration, exactly as it did when it ran for Plain. The reason it is zero is that Custom writes its own name method and never touches this body. The same source line runs four times for one type while never running for another; the run count does not measure whether the body exists, it measures which types use it.

This distinction will come up again through the rest of the course: none of the five bodies in the never class have been deleted from the source — all five were not triggered even once in this run. A count of zero does not mean “this code is unnecessary”; it means “this path was not chosen in this run.”

The default body itself was also written to make this visible: its key string is built by placing the value returned by the self.kind() call into a pattern, not a fixed string. The trait’s own body never knows the name of the type running it — self.kind() produces a string, and the body borrows it and appends it to the key. This lets a single default body be counted in a separate row for every type that inherits it; even if Plain and Custom shared the same default body (in this example they do not, because Custom wrote its own), they could still be counted separately under two separate keys. This small design decision also explains why the kernel can be sliced in the rest of the lessons: every body carries its own key, so taking one slice does not affect the remaining keys.

Summary

  • A trait establishes a type-independent behavior contract and can carry a default body that takes over when the implementing type does not write one.
  • The measurement kernel classifies sixteen bodies: 8 ran every time, 3 only when needed, 5 never — this number does not change through the course.
  • The same default body runs four times (every time) for one type while running zero times (never) for a type that overrides it — the body in the source is the same, the run count differs.
  • trait and interface are separate concepts: interface was defined in Go on the axis of where the binding is established, trait here is a contract established with an explicit declaration that can carry a default body.
  • A run count of zero does not mean the body is missing from the source; it means that path was never taken for that type.

Next Step

The body of a type implementing a trait could be measured, but to use a body inside a generic function, that function’s type parameter needs a bound. The next lesson asks whether this bound touches the run count at all — the same body is written and run in both a bounded and an unbounded form.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close