Prefer **obvious, readable Racket** over cleverness. ## Control flow ```racket ;; Good: match for destructuring (match-define (list name age) (get-user-info id)) ;; Good: cond over nested if (cond [(empty? items) '()] [(special? (first items)) (handle-special items)] [else (process-normal items)]) ``` - Use `match` / `match-define` for destructuring. - Use `cond`, `case`, `and`, `or` cleanly; avoid nested `if` pyramids. - Prefer `define` over `let`/`let*` when it reduces indentation. - Use `let*` when bindings depend on earlier bindings; use `let` for independent bindings. ## Iteration idioms ```racket ;; Prefer for/* with explicit sequence types (for/list ([x (in-list items)] ; in-list for performance [i (in-naturals)]) ; in-naturals for indices (process x i)) ;; for/fold for accumulation (for/fold ([acc '()] [seen (set)]) ([x (in-list items)]) (values (cons (transform x) acc) (set-add seen x))) ``` - Use `for/*` loops over manual recursion unless recursion is clearer. - Use `in-list`, `in-vector`, `in-hash`, etc. explicitly — faster than generic sequence. - Use `for/fold` for complex accumulation; `for/list`, `for/hash` for simple transforms. - Use `for*/list` (note the `*`) when you need nested iteration flattened. ## Data structure mutability - **Immutable by default**: lists, immutable hashes, immutable vectors for most code. - **Mutable when**: you need O(1) update in a hot loop, or modeling inherently stateful things. - **Mutable hashes** (`make-hash`): use for caches, memoization, symbol tables. - **Mutable vectors** (`make-vector`): use for fixed-size buffers, matrix ops. - **Boxes** (`box`, `unbox`, `set-box!`): use for single mutable cells, rarely needed. - Don't mix: if a data structure is mutable, keep it internal; expose immutable views. ## Module hygiene ```racket ;; Good: explicit contract-out, interface at top (provide (contract-out [process-data (-> input/c output/c)] [make-processor (-> config/c processor/c)])) (require racket/match "internal-utils.rkt") ``` - **One abstraction per module** (~500 lines rule of thumb). - Put `provide` before `require` — interface at top. - Use `contract-out` when correctness matters (public APIs, callbacks, data shapes). - Use explicit `provide` lists only — never `(all-defined-out)` in production. - Use `racket/base` for libraries (faster loading); `racket` for scripts. ## Contracts: when and how much - **Module boundaries**: use `contract-out` for public APIs — catches bugs at the boundary with clear blame. - **Internal functions**: use `define/contract` sparingly for tricky invariants or during debugging. - **Higher-order contracts**: use `->` for simple functions; `->i` when you need dependent contracts. - **In tests**: contracts give fast feedback — keep them on during development, consider `#:unprotected-submodule` for perf-critical production paths. - **Don't go nuts**: contracts at every internal function add overhead and noise. Focus on boundaries. ## Naming - Prefix functions with data type of main argument: `board-ref`, `board-free-spaces`, not `ref`, `free-spaces`. - Use `-ref`, `-set`, `-update` suffixes for accessors/mutators on custom types. - Avoid abbreviations except well-known ones (`idx`, `len`, `ctx`). ## Data modeling - Prefer `struct` (possibly `#:transparent`) for domain objects, not ad-hoc hash soup. - For enums/variants: `struct` variants + `match`, or symbols with clear validation. - Validate external data (YAML/JSON) once at the boundary; keep internal representation consistent. ## Error handling - Use `raise-argument-error`, `raise-user-error`, or `error` with a clear message. - Wrap IO and parsing with `with-handlers` and rethrow with context (what file, what phase). ## IO and paths - Use `build-path`, `simplify-path`, `path->string`; don't concatenate path strings manually. - Use `call-with-input-file` / `call-with-output-file` idiomatically. ## Macros: use sparingly - Don't write macros unless it meaningfully reduces boilerplate or enforces invariants. - If writing macros: use `syntax-parse` (not raw `syntax-case`) and include good error messages. - Keep macro output readable and debuggable. ## Phase separation - Understand `for-syntax` vs runtime; don't accidentally pull runtime values into macros. - Use `begin-for-syntax` sparingly; prefer `syntax-local-value` patterns when possible. ## Continuations: use sparingly - Prefer `call/ec` (escape continuations) over full `call/cc` when possible. - Use `parameterize` for dynamic scope, not continuation tricks. - If using `parameterize`, keep scope tight and document effects. ## Concurrency - Use `place`s for CPU parallelism, `thread`s for I/O concurrency. - Prefer channels (`make-channel`, `channel-put`, `channel-get`) over shared state. - Use `sync` and events for composable waiting. ## Gotchas - `eq?` vs `equal?` vs `eqv?`: use `equal?` by default for structural comparison. - `null?` only works on proper lists; use `empty?` from `racket/list` for generics. - `string=?` not `equal?` for string comparison in hot paths. ## Testing - Use `module+ test` submodules; run with `raco test`. - Add `rackunit` tests for tricky logic; prefer table-driven `test-case` with `check-equal?`. - Consider Scribble docs for library boundaries. ## Size heuristics - **One abstraction per module** — if you're documenting two unrelated things, split. - **One screen per function** (~66 lines) — if you can't see the whole function, extract helpers.