Rung 3: Structs, enums, Option and Result

Data as types, match, Option, Result and ?. Why a refusal is a value and a hole is never false.

Rust lets you say what your data is, exactly, in types. A struct says "all of these together". An enum says "exactly one of these". Option says "maybe nothing", and Result says "this or a reason it failed". Gestalt leans on this hard: a refusal is a value with a class and a code, and a missing fact is a hole with a reason. Neither is ever false, 0 or a crash.

The idea #

Structs: fields together #

struct Refusal {
    class: Class,
    code: &'static str,
    next: Option<Next>,
}

struct Digest(pub [u8; 32]);   // a "newtype": one field, a type of its own

A newtype costs nothing at run time. But a Digest can no longer be passed where some other 32 bytes are wanted, like a key. Read the field with .0.

Methods go in an impl block. &self borrows the value; &mut self may change it; self consumes it.

#[derive(Debug, Clone, Copy, PartialEq, Eq)] asks the compiler to write the obvious code: printing, copying, comparing.

Enums: exactly one of these #

enum Value {
    U(u64),          // a variant can carry data
    T(String),
    Bool(bool),
    Null,            // or none
}

A Value is one of these and nothing else. There is no "null pointer" and no "unknown kind".

match: every case, or it won't build #

fn kind(v: &Value) -> &'static str {
    match v {
        Value::U(_) => "unsigned",
        Value::T(_) => "text",
        Value::Bool(_) => "bool",
        Value::Null => "null",
    }
}

A match must cover every variant. Add a variant to the enum, and the compiler lists every match that has to learn about it. So a catch-all _ => arm should be a choice, not a habit. Patterns can take data out (Value::U(n) => n), match ranges (0..=23) and match pairs ((Some(a), Some(b))).

Option: maybe nothing #

enum Option<T> { Some(T), None }

Option<u64> is not a number. You can't add it to anything until you say what None means. if let Some(n) = x { ... } and let Some(n) = x else { return ... }; take the value out or handle its absence.

Result and ?: this, or why not #

enum Result<T, E> { Ok(T), Err(E) }

fn byte(&mut self) -> Result<u8, CborError> {
    let x = *self.bytes.get(self.at).ok_or(CborError::Truncated)?;
    self.at += 1;
    Ok(x)
}

? means "if this is an Err, return it now; otherwise give me the value". A chain of steps that can each fail reads like a straight line. The first failure ends it.

Three converters you will use all the time:

  • .ok_or(e) turns an Option into a Result: None becomes Err(e).
  • .map_err(|_| e) swaps a Result's error for yours.
  • .checked_add(b) returns None instead of overflowing.

.unwrap() and .expect("...") take the value or panic. They are fine in tests and drills, and never right for bytes that came from a stranger.

Why Gestalt cares #

  • A refusal is a value. When admit refuses an entry, it returns a Refusal: a class, a stable code, what to do next, and what it was about. The caller can read it, show it, and match on it. Nothing is thrown and nothing crashes.
  • A hole is never false. When derive can't answer, because an input is missing, a rule is absent or a sum overflows, it returns a hole with a Reason. Gestalt's fixed rule: a missing fact is never false, and a withheld one is never "no". Option and Result make that rule something the compiler enforces.
  • Closed lists. Refusal classes, hole reasons and CBOR errors are enums, closed lists with stable codes that the spec's vectors name. A new reason can't slip in unnoticed: every match on the list must learn it.
  • Never crash on strangers' bytes. A node decodes whatever arrives. The decoder returns Err for every defect, so bad input can't take the node down. Exercise 7 checks your decoder against every one- and two-byte input there is.

In Gestalt's code #

The codec's one data item. Your Value is this without arrays and maps.

From engine/kernel/codec/src/cbor.rs:39:

pub enum Value {
    /// An unsigned integer.
    U(u64),
    /// A negative integer, `-1 - n`.
    N(u64),
    /// A byte string.
    B(Vec<u8>),
    /// A text string.
    T(String),
    /// An array.
    A(Vec<Value>),
    /// A map with unsigned-integer keys.
    M(Map),
    /// `true` or `false`.
    Bool(bool),
    /// `null`.
    Null,
}

decode is your exercise 7, with the reader's ? doing the work. ok_or and ? together in one line is the engine's own byte (cbor.rs:367).

From engine/kernel/codec/src/cbor.rs:341:

pub fn decode(bytes: &[u8]) -> Result<Value, CborError> {
    let mut r = Reader { b: bytes, at: 0 };
    let v = r.item()?;
    if r.at != bytes.len() {
        return Err(CborError::TrailingBytes);
    }
    Ok(v)
}

A refusal: a struct of enums and options. next is an Option because not every refusal has a next step.

From engine/kernel/admit/src/refusal.rs:54:

pub struct Refusal {
    /// The class.
    pub class: Class,
    /// The stable code (the conformance cases and the node name it).
    pub code: &'static str,
    /// What the caller can do next.
    pub next: Option<Next>,
    /// The entry, standing, float or module the refusal is about.
    pub about: Option<Digest>,
    // ...

A hole, and the arithmetic that makes one. Derive computes in i128 and checks every step. Your total in exercise 8 is the same idea.

From engine/kernel/derive/src/hole.rs:1 and engine/kernel/derive/src/num.rs:57:

//! Holes: questions not yet answered, with their reason (ANSWER-17).
//! ... A missing fact is never false and a withheld one is never "no":
//! both are holes (fixed truth 8, PA175).

pub fn add(a: i128, b: i128) -> Result<i128, Reason> {
    a.checked_add(b).ok_or(Reason::Overflow)
}

Exercise #

cd ~/Documents/gestalt-learn
./ladder 3

Open rungs/03-types/src/lib.rs. The helpers from rungs 1 and 2 are given, written as the engine writes them. Eight exercises take you from a newtype to a full canonical decoder. One test feeds your decoder all 65,792 inputs of one and two bytes. It must never panic, and whatever it accepts must encode back to the same bytes. That is the codec's own promise.

Then three drills:

./ladder drill 3 every_case
./ladder drill 3 question_mark
./ladder drill 3 unknown_is_not_zero

Stuck? rungs/03-types/solution/ has one way to do it, and ./ladder solution 3 runs the tests against it.

Check yourself #

  • Why is a match without a catch-all safer when the enum grows?
  • What does ? do, and where can you use it?
  • Why is an unknown amount a hole and not 0?
  • Why must the decoder return Err rather than panic?

Sign in to mark your progress

Sign in or create a learner to mark the lesson and the exercise, and to review this rung's cards.

The cards (15)

Try each one before you open its answer. Review them on a schedule from the Review page.

  1. Struct or enum: which says "all of these together" and which "exactly one of these"?

    Answer

    A struct holds all its fields together. An enum is exactly one of its variants, and a variant can carry data of its own.

  2. What is a newtype like struct Digest(pub [u8; 32]) for?

    Answer

    It gives the bytes a type of their own at no run-time cost, so a digest can't be passed where a key or other 32 bytes are wanted. Read the inside with .0.

    See engine/kernel/codec/src/hash.rs:20

  3. Why is a match with no catch-all _ arm safer when an enum grows?

    Answer

    match must cover every variant. Add a variant and the compiler refuses every match that doesn't handle it yet, so none is missed.

  4. What does ? do after a Result?

    Answer

    On Err(e) it returns Err(e) from the function at once; on Ok(v) it gives v. It only works in a function that itself returns Result (or, for Option, Option).

    See engine/kernel/codec/src/cbor.rs:341

  5. How do you turn an Option into a Result?

    Answer

    .ok_or(error): Some(v) becomes Ok(v) and None becomes Err(error). The codec's reader: *self.b.get(self.at).ok_or(CborError::Truncated)?.

    See engine/kernel/codec/src/cbor.rs:367

  6. What does checked_add return, and why does derive use it?

    Answer

    Some(sum), or None if the sum overflows. Derive turns that None into a hole, Reason::Overflow, instead of a wrong number.

    See engine/kernel/derive/src/num.rs:57

  7. Gestalt's fixed truth about a missing fact?

    Answer

    A missing fact is never false, and a withheld one is never "no": both are holes, with a reason.

    See engine/kernel/derive/src/hole.rs:1

  8. Why can't you add two Option<u64> values?

    Answer

    An Option<u64> may be no number at all. You must say what happens when one is None. In Gestalt the answer is a hole, never a 0 standing in for "unknown".

  9. What is a refusal in Gestalt, as a Rust type?

    Answer

    A Refusal struct: a Class (structural, authorization, operation, company), a stable code, an Option<Next> (propose or get), and what it is about. It is a value the caller reads, never a crash.

    See engine/kernel/admit/src/refusal.rs:54

  10. What are the four classes of refusal?

    Answer

    Structural (bytes, type, schema, parents, worldline or time break a rule); Authorization (the signer, standing, pen rule or float doesn't carry the act); Operation (the head's own policy or clock, or the record moved on); Company (the company's own control, reserved).

    See engine/kernel/admit/src/refusal.rs:14

  11. When are .unwrap() and .expect() acceptable?

    Answer

    In tests, drills and cases that truly can't fail. Never on bytes from a stranger: a node must answer bad input with Err, not a panic.

  12. The codec's promise about decode and encode?

    Answer

    decode refuses every non-canonical form, so a value that decodes always encodes back to exactly the same bytes. One value, one encoding, one hash.

    See engine/kernel/codec/src/cbor.rs:1

  13. In CBOR, what are 0xf4, 0xf5 and 0xf6?

    Answer

    false, true and null: major type 7 with low bits 20, 21 and 22.

  14. Why do Gestalt's error and hole lists carry stable codes, like non-shortest or missing?

    Answer

    The spec's vectors and conformance cases name them. Any implementation must refuse the same input with the same code, and a code never changes meaning.

    See engine/kernel/codec/src/cbor.rs:234

  15. What does let Some(a) = x else { return Err(Reason::Missing) }; do?

    Answer

    If x is Some, it names the value a and carries on. If x is None, it runs the else block, which must leave the function (or the loop).