Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 17: The Prelude and Deriving

New files

  • src/derive.rs

Changed files

  • src/primitives.rs
  • src/desugar.rs
  • src/ast.rs
  • src/core_lint.rs
  • src/eval.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/rename.rs

Motivation

A language starts to feel like a language when you can reach for map, foldr, or maybe without writing them yourself every time. We now have enough Fun to write its standard library in Fun. That’s a good milestone!

We’ll also teach the compiler to write the repetitive parts. Given a datatype, deriving generates an ordinary instance for equality, ordering, or display. The generated code goes through the same checker as code we’d write by hand. There’s no secret equality operation in the interpreter waiting to rescue it.

This chapter brings the interpreted language together. Take a little time to play with it before we head into the back end. Small library functions are also excellent compiler examples. Their types and definitions often tell the whole story in just a few lines.

The Language, Now

A datatype may request deriving Eq or a nonempty parenthesized list drawn from Eq, Ord, and Show. This example constructs its record fields in a different order from their declaration:

data P a = P {x :: a, y :: Int} deriving (Eq, Ord, Show)

main = print (Just (P {y=2,x=Left 1} :: P (Either Int Bool)))

Derived equality compares constructors and then their fields. Ordering uses constructor declaration order, followed by lexicographic field order. Derived Show prints record fields in declaration order, regardless of construction order, and inserts parentheses around nested constructor applications where needed. Lists use comma-separated brackets. [Char] is still a list, not a string. Functions have no derived comparison or display instance.

The completed Prelude provides list folds and traversals, finite generation, string splitting and joining, numeric conversions, character operations, and monadic control. [a..b] is an inclusive Int range. iterateN produces a bounded sequence. Infinite lists are unavailable because constructor fields are strict. foldl' and foldl have the same strict behavior here.

Use ++ to append lists and <> to append strings. Numeric literals still have fixed types, so sum and product operate on Int lists. Their starting values are integer literals, and Fun’s Num class has no operation for turning a literal into an arbitrary numeric type.

Putting Small Functions Together

Now we can write something compact enough to read in one breath.

total = sum . map (\x -> x * x) . filter even

main = print (total [1..10])

This prints 220. Read the composition from right to left. Keep the even numbers, square each one, then add them up. Each function does one familiar job, and (.) connects the result of one to the input of the next. We don’t have to name a loop index or describe how to advance through the list.

That is a beautiful way to express the calculation. It doesn’t mean the work has vanished. Fun is strict, and this program builds intermediate lists. Later optimizations could remove them, but the useful separation is already here. We can understand what the program means before choosing how to make it faster.

Theory

Imagine writing equality for a tree by hand. Empty trees are equal. Two nodes are equal when their corresponding fields are equal. Different constructors aren’t equal. The datatype declaration already tells us enough to write those clauses, so let’s have the compiler write them.

That’s deriving. We generate an ordinary instance, then check it just as we would check a handwritten one. The Haskell Report specifies the structural behavior (Simon Marlow 2010). We adapt it to Fun’s records, primitive strings, and smaller Show interface.

A parameter used in a field usually introduces a corresponding instance constraint. A phantom parameter does not. For data Token a = Token Int, equality needs no Eq a dictionary because no value of type a is examined. Type synonyms must be expanded before deciding which parameters occur in fields. The normal instance solver then checks whether the generated context actually suffices for every field operation (Jones 1999). Deriving is not an escape from those checks.

Printing constructors has a small but important wrinkle. Just (Left 3) needs parentheses, but Left 3 on its own doesn’t. A function of type a -> String can’t see where its result will be used. We need to pass that surrounding precedence in as well.

Fun’s Show dictionary has an internal _showsPrec method for this purpose. Handwritten instances inherit a default, while derived instances implement it structurally. Top-level show starts at precedence zero. Positional constructor arguments use eleven, which asks nested constructor applications to parenthesize themselves. Record fields use ordinary show inside their labelled braces.

Strictness changes another familiar library definition. An ordinary top-level undefined = error "undefined" would fail during startup, even if unused. Instead, an unshadowed reference to the Prelude’s undefined is a scoped abbreviation for applying a unit-taking helper. This preserves failure at the use site without introducing a thunk or a lazy binding. The evaluation-strategy distinction from Chapter 5 remains essential (Pierce 2002).

Implementation

derive::derive runs immediately before class collection and lexical renaming. Generated terms retain the datatype’s source span, so a rejected field operation can point back to the declaration. The pass expands aliases when collecting used parameters, selects one of three generators, and appends ordinary instance declarations:

#![allow(unused)]
fn main() {
use crate::{
    ast::*,
    diagnostics::{Diagnostic, ErrorCode},
    session::{Session, Stop},
    source::Span,
};
use std::collections::BTreeSet;
struct Builder {
    span: Span,
}
impl Builder {
    fn expr(&self, kind: Kind) -> Expr {
        Expr {
            kind,
            span: self.span,
        }
    }
    fn var(&self, name: &str) -> Expr {
        self.expr(Kind::Var(name.into()))
    }
    fn string(&self, text: &str) -> Expr {
        self.expr(Kind::String(text.into()))
    }
    fn call(&self, name: &str, args: Vec<Expr>) -> Expr {
        args.into_iter().fold(self.var(name), |f, x| {
            self.expr(Kind::App(Box::new(f), Box::new(x)))
        })
    }
    fn pat(&self, name: &str) -> Pat {
        Pat {
            kind: PatKind::Var(name.into()),
            span: self.span,
        }
    }
    fn constructor(&self, c: &ConstructorDecl, prefix: &str) -> Pat {
        Pat {
            kind: PatKind::Con(
                c.name.clone(),
                (0..c.fields.len())
                    .map(|i| self.pat(&format!("{prefix}{i}")))
                    .collect(),
            ),
            span: self.span,
        }
    }
    fn row(&self, patterns: Vec<Pat>, expr: Expr) -> Row {
        Row {
            patterns,
            body: RowBody::Plain(Box::new(expr)),
            locals: vec![],
            span: self.span,
        }
    }
    fn binding(&self, name: &str, rows: Vec<Row>) -> Binding {
        Binding {
            name: name.into(),
            rhs: self.expr(Kind::Equations(rows)),
            nonrecursive: vec![],
            span: self.span,
        }
    }
    fn append(&self, pieces: Vec<Expr>) -> Expr {
        pieces
            .into_iter()
            .reduce(|a, b| self.call("<>", vec![a, b]))
            .unwrap_or_else(|| self.string(""))
    }
    fn eq(&self, d: &DataDecl) -> Binding {
        let mut rows = vec![];
        for (i, a) in d.constructors.iter().enumerate() {
            for (j, b) in d.constructors.iter().enumerate() {
                let mut rhs =
                    self.var(if i == j { "True" } else { "False" });
                if i == j {
                    for k in (0..a.fields.len()).rev() {
                        let test = self.call(
                            "==",
                            vec![
                                self.var(&format!("x{k}")),
                                self.var(&format!("y{k}")),
                            ],
                        );
                        rhs = self.expr(Kind::If(
                            Box::new(test),
                            Box::new(rhs),
                            Box::new(self.var("False")),
                        ));
                    }
                }
                rows.push(self.row(
                    vec![
                        self.constructor(a, "x"),
                        self.constructor(b, "y"),
                    ],
                    rhs,
                ));
            }
        }
        self.binding("==", rows)
    }
    fn ord(&self, d: &DataDecl) -> Binding {
        let mut rows = vec![];
        for (i, a) in d.constructors.iter().enumerate() {
            for (j, b) in d.constructors.iter().enumerate() {
                let mut rhs = self.var(if i < j {
                    "LT"
                } else if i > j {
                    "GT"
                } else {
                    "EQ"
                });
                if i == j {
                    for k in (0..a.fields.len()).rev() {
                        let comparison = self.call(
                            "compare",
                            vec![
                                self.var(&format!("x{k}")),
                                self.var(&format!("y{k}")),
                            ],
                        );
                        let equal = Pat {
                            kind: PatKind::Con("EQ".into(), vec![]),
                            span: self.span,
                        };
                        rhs = self.expr(Kind::Case(
                            Box::new(comparison),
                            vec![
                                self.row(vec![equal], rhs),
                                self.row(
                                    vec![self.pat("order")],
                                    self.var("order"),
                                ),
                            ],
                        ));
                    }
                }
                rows.push(self.row(
                    vec![
                        self.constructor(a, "x"),
                        self.constructor(b, "y"),
                    ],
                    rhs,
                ));
            }
        }
        self.binding("compare", rows)
    }
    fn show(&self, d: &DataDecl) -> Vec<Binding> {
        let mut rows = vec![];
        for c in &d.constructors {
            let mut pieces = vec![self.string(&c.name)];
            if let Some(labels) = &c.labels {
                pieces.push(self.string(" {"));
                for (i, label) in labels.iter().enumerate() {
                    pieces.push(self.string(&format!(
                        "{}{label} = ",
                        if i == 0 { "" } else { ", " }
                    )));
                    pieces.push(
                        self.call("show", vec![self.var(&format!("x{i}"))]),
                    );
                }
                pieces.push(self.string("}"));
            } else {
                for i in 0..c.fields.len() {
                    pieces.push(self.string(" "));
                    pieces.push(self.call(
                        "_showsPrec",
                        vec![
                            self.expr(Kind::Int(11)),
                            self.var(&format!("x{i}")),
                        ],
                    ));
                }
            }
            let mut rhs = self.append(pieces);
            if !c.fields.is_empty() {
                let test = self.call(
                    ">",
                    vec![self.var("p"), self.expr(Kind::Int(10))],
                );
                rhs = self.call("_showParen", vec![test, rhs]);
            }
            rows.push(
                self.row(
                    vec![self.pat("p"), self.constructor(c, "x")],
                    rhs,
                ),
            );
        }
        let show = self.binding(
            "show",
            vec![self.row(
                vec![self.pat("value")],
                self.call(
                    "_showsPrec",
                    vec![self.expr(Kind::Int(0)), self.var("value")],
                ),
            )],
        );
        vec![show, self.binding("_showsPrec", rows)]
    }
}
fn variables(ty: &TypeSyntax, out: &mut BTreeSet<String>) {
    match ty {
        TypeSyntax::Name(n) => {
            out.insert(n.clone());
        }
        TypeSyntax::App(a, b) | TypeSyntax::Arrow(a, b) => {
            variables(a, out);
            variables(b, out);
        }
        TypeSyntax::Qualified(_, t) => variables(t, out),
    }
}
/// Generate only Eq, Ord and Show. Generated terms retain the declaration span.
pub fn derive(
    sess: &mut Session,
    program: &mut Program,
) -> Result<(), Stop> {
    for d in &program.data {
        if d.deriving.is_empty() {
            continue;
        }
        let build = Builder { span: d.span };
        let mut used = BTreeSet::new();
        for c in &d.constructors {
            for ty in &c.fields {
                let expanded =
                    crate::kinds::expand(ty, &program.aliases, d.span)
                        .map_err(|e| {
                            sess.emit(e);
                            Stop
                        })?;
                variables(&expanded, &mut used);
            }
        }
        for class in &d.deriving {
            let methods = match class.as_str() {
                "Eq" => vec![build.eq(d)],
                "Ord" => vec![build.ord(d)],
                "Show" => build.show(d),
                _ => {
                    sess.emit(Diagnostic::error(
                        ErrorCode::Instance,
                        "only Eq, Ord and Show can be derived",
                        d.span,
                    ));
                    return Err(Stop);
                }
            };
            let context = d
                .parameters
                .iter()
                .filter(|p| used.contains(*p))
                .map(|p| PredicateSyntax {
                    class: class.clone(),
                    variable: p.clone(),
                })
                .collect();
            let head = d.parameters.iter().fold(
                TypeSyntax::Name(d.name.clone()),
                |a, p| {
                    TypeSyntax::App(
                        Box::new(a),
                        Box::new(TypeSyntax::Name(p.clone())),
                    )
                },
            );
            program.instances.push(InstanceDecl {
                class: class.clone(),
                head,
                context,
                methods,
                span: d.span,
            });
        }
    }
    Ok(())
}
}

The builders construct the existing surface expression and pattern types. Equality generates a clause for each pair of constructors. Unequal constructors return False. Matching constructors compare fields from left to right, stopping at the first unequal pair:

#![allow(unused)]
fn main() {
fn eq(&self, d: &DataDecl) -> Binding {
    let mut rows = vec![];
    for (i, a) in d.constructors.iter().enumerate() {
        for (j, b) in d.constructors.iter().enumerate() {
            let mut rhs =
                self.var(if i == j { "True" } else { "False" });
            if i == j {
                for k in (0..a.fields.len()).rev() {
                    let test = self.call(
                        "==",
                        vec![
                            self.var(&format!("x{k}")),
                            self.var(&format!("y{k}")),
                        ],
                    );
                    rhs = self.expr(Kind::If(
                        Box::new(test),
                        Box::new(rhs),
                        Box::new(self.var("False")),
                    ));
                }
            }
            rows.push(self.row(
                vec![
                    self.constructor(a, "x"),
                    self.constructor(b, "y"),
                ],
                rhs,
            ));
        }
    }
    self.binding("==", rows)
}
}

Ordering follows the same constructor-pair structure. For a matching pair it compares each field, continuing only when the result is EQ. Show instead builds string expressions from constructor names, field labels, punctuation, and recursive _showsPrec calls. These expressions pass through ordinary renaming, inference, dictionary elaboration, match compilation, and Core lint. Requesting an unsupported class reports E0211. Requesting Ord without the required equality evidence also fails through the existing instance checks.

The library lives in prelude.fun, embedded ahead of the user program. Its private helper references are resolved within that scope, so a user binding cannot silently change the implementation of a Prelude function. Inclusive ranges check the upper bound before incrementing. A range ending at the largest 63-bit integer must terminate rather than wrap around.

Character classification and simple case conversion use checked-in Unicode 17.0.0 tables in unicode.fun. tools/unicode.py records the upstream source and its digest, and the data license is retained. The generated tables are ordinary compact Fun strings. General-category predicates and simple case mappings therefore execute through the language rather than adding a large set of new primitives. isDigit deliberately recognizes ASCII decimal digits. Not every character with a Unicode numeric classification satisfies it.

A library also teaches us where the compiler is doing unnecessary work. Walking a list shouldn’t copy its entire tail at every step. We share immutable constructor field vectors with Rc, and Core lint removes local bindings on scope exit instead of copying its whole environment. These are useful reminders to look at the cost of our Rust representation as well as the Fun program.

Running It

$ cd crates/ch17_prelude
$ cargo run -q -- run tests/programs/prelude/derive-show-record.fun --lint
Just (P {x = Left 1, y = 2})
$ cargo run -q -- dump --pass=types tests/programs/prelude/derive-show-record.fun
main : IO ()

The output demonstrates both declaration-order record fields and parentheses around the record constructor beneath Just. --pass=core shows the generated instance dictionaries. --pass=match shows their compiled structural tests. The separate derive-show-tree and derive-show-literals fixtures cover nested applications and escaping.

Library edge cases make good experiments. Try a singleton range at the largest Int, an empty list passed to a fold, or a derived instance with a phantom parameter. The little examples are doing real work here. A boundary case can explain a library definition more clearly than a long ordinary input.

Exercises

  1. Warm-up. Derive Eq, Ord, and Show for a two-constructor tree and compare the result with a handwritten instance.
  2. Intermediate. Add an unused datatype parameter and inspect whether its dictionary appears in the derived instance context.
  3. Intermediate. Trace the precedence passed while displaying Just (Left (Just 3)).
  4. Intermediate. Test a singleton range at the maximum Int and explain why the order of the bound check and increment matters.
  5. Advanced. Measure constructor-tail copying on a Unicode lookup before and after immutable field-vector sharing, without changing the Fun program.

Further Reading