Refine carmack.racket.md with deeper Racket idioms

Major improvements:
- Iteration idioms: for/fold example, for*/list, in-naturals for indices
- Data structure mutability: when to use mutable hash/vector/box
- let/let*/define style: use let* when order matters
- Contracts section: when to use define/contract, ->i, boundary focus
- Naming: -ref/-set/-update suffixes for custom types
- Size heuristics: semantic ('one abstraction per module') not numeric
- Module hygiene: explicit provides only, contract-out when correctness matters

Removed:
- Packages/tooling section (covered in base racket.md injection)

Now 119 lines of actionable, non-obvious Racket guidance.
This commit is contained in:
Dhanji R. Prasanna
2026-01-15 07:43:04 +05:30
parent e222b9affc
commit 52cd19a015

View File

@@ -1,6 +1,6 @@
Prefer **obvious, readable Racket** over cleverness. Prefer **obvious, readable Racket** over cleverness.
## Keep control flow clean ## Control flow
```racket ```racket
;; Good: match for destructuring ;; Good: match for destructuring
(match-define (list name age) (get-user-info id)) (match-define (list name age) (get-user-info id))
@@ -12,39 +12,69 @@ Prefer **obvious, readable Racket** over cleverness.
[else (process-normal items)]) [else (process-normal items)])
``` ```
- Use `match` / `match-define` for destructuring. - Use `match` / `match-define` for destructuring.
- Use `for/*` loops and sequences instead of manual recursion unless recursion is clearer.
- Use `cond`, `case`, `and`, `or` cleanly; avoid nested `if` pyramids. - Use `cond`, `case`, `and`, `or` cleanly; avoid nested `if` pyramids.
- Prefer `define` over `let`/`let*` when it reduces indentation. - Prefer `define` over `let`/`let*` when it reduces indentation.
- Use immutable data by default; reach for mutation only when it materially improves clarity/perf. - Use `let*` when bindings depend on earlier bindings; use `let` for independent bindings.
## Modules: explicit exports ## Iteration idioms
```racket ```racket
;; Good: explicit contract-out ;; 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 (provide
(contract-out (contract-out
[process-data (-> input/c output/c)])) [process-data (-> input/c output/c)]
[make-processor (-> config/c processor/c)]))
;; Bad: leaky exports (require racket/match
(provide (all-defined-out)) "internal-utils.rkt")
``` ```
- Organize code into small modules with explicit `provide` lists. - **One abstraction per module** (~500 lines rule of thumb).
- Put `provide` before `require` — interface at top. - Put `provide` before `require` — interface at top.
- Prefer `contract-out` when exporting public APIs. - Use `contract-out` when correctness matters (public APIs, callbacks, data shapes).
- Avoid `provide (all-defined-out)` except in quick prototypes/tests. - Use explicit `provide` lists only — never `(all-defined-out)` in production.
- Prefer `require` with explicit identifiers; avoid huge wildcard imports. - Use `racket/base` for libraries (faster loading); `racket` for scripts.
- Use `racket/base` for libraries (faster loading); use `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 ## Naming
- Prefix functions with data type of main argument: `board-free-spaces` not `free-spaces`. - 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.
## Contracts and types - Avoid abbreviations except well-known ones (`idx`, `len`, `ctx`).
- If in untyped Racket: add contracts at module boundaries for public APIs, especially for callbacks and data shapes.
- If the project uses `typed/racket`, keep typed/untyped boundaries clean and document them.
- Use predicates + struct definitions to make data models explicit.
## Data modeling ## Data modeling
- Prefer `struct` (possibly `#:transparent`) for domain objects, not ad-hoc hash soup. - Prefer `struct` (possibly `#:transparent`) for domain objects, not ad-hoc hash soup.
- For enums/variants: consider `struct` variants + `match`, or symbols with clear validation. - For enums/variants: `struct` variants + `match`, or symbols with clear validation.
- Validate external data (YAML/JSON) once at the boundary; keep internal representation consistent. - Validate external data (YAML/JSON) once at the boundary; keep internal representation consistent.
## Error handling ## Error handling
@@ -55,12 +85,6 @@ Prefer **obvious, readable Racket** over cleverness.
- Use `build-path`, `simplify-path`, `path->string`; don't concatenate path strings manually. - Use `build-path`, `simplify-path`, `path->string`; don't concatenate path strings manually.
- Use `call-with-input-file` / `call-with-output-file` idiomatically. - Use `call-with-input-file` / `call-with-output-file` idiomatically.
## Performance
- Prefer vectors for hot loops / indexed access; lists for iteration; hashes for keyed lookup.
- Use `for/fold` or `for/hash` to build results efficiently.
- Use `in-list`, `in-vector`, etc. explicitly in `for` loops for better performance.
- Avoid repeated `append` in loops; accumulate then reverse if needed.
## Macros: use sparingly ## Macros: use sparingly
- Don't write macros unless it meaningfully reduces boilerplate or enforces invariants. - 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. - If writing macros: use `syntax-parse` (not raw `syntax-case`) and include good error messages.
@@ -86,15 +110,10 @@ Prefer **obvious, readable Racket** over cleverness.
- `string=?` not `equal?` for string comparison in hot paths. - `string=?` not `equal?` for string comparison in hot paths.
## Testing ## Testing
- Add `rackunit` tests for tricky logic; prefer table-driven tests.
- Use `module+ test` submodules; run with `raco test`. - 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. - Consider Scribble docs for library boundaries.
## Size limits ## Size heuristics
- ~500 lines per module (1000 tolerable, 10000 is a god-file). - **One abstraction per module** — if you're documenting two unrelated things, split.
- ~66 lines per function (one screen). - **One screen per function** (~66 lines) — if you can't see the whole function, extract helpers.
## Packages/tooling
- Assume `raco fmt` style; keep formatting consistent.
- Use `raco pkg install --auto` for dependency resolution.
- Prefer `info.rkt` for package metadata over ad-hoc scripts.