Files
g3/prompts/langs/carmack.racket.md
Dhanji R. Prasanna 616e0898c7 Add performance deep cuts and parameterize guidance
Performance:
- Beware list-ref in a loop (O(n²) trap)
- Consolidated performance section with data structure selection rationale
- for/fold for single-pass result building

Parameters and dynamic scope:
- Good uses: ports, logging, config, test fixtures
- Bad uses: hidden global state, implicit argument passing
- Document when functions read from parameters

Also simplified Continuations section (parameterize now has its own section).
2026-01-15 13:49:29 +05:30

133 lines
6.3 KiB
Markdown

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.
## Performance
- Use `in-list`, `in-vector`, `in-hash` explicitly in `for` loops — faster than generic sequence.
- **Beware `list-ref` in a loop** — it's O(n) per call, so O(n²) overall. Use vectors for indexed access.
- Don't repeatedly `append` in loops; use `for/list` or accumulate with `cons` then `reverse`.
- Prefer vectors for indexed access, hashes for keyed lookup, lists for sequential iteration.
- Use `for/fold` to build results in one pass instead of multiple traversals.
## 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.
## Parameters and dynamic scope
- **Good uses**: current ports, logging context, configuration, test fixtures.
- **Bad uses**: hidden global state that affects correctness, implicit arguments to avoid passing data.
- Keep `parameterize` scope tight — wrap the smallest expression that needs it.
- Document when a function reads from a parameter (it's implicit input).
- Prefer explicit arguments over parameters when the caller should always think about the value.
## 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
- Prefer `call/ec` (escape continuations) over full `call/cc` — simpler, faster, sufficient for early exit.
- Don't use continuations for what `parameterize` or exceptions handle better.
## 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.