Files
g3/prompts/langs/carmack.racket.md
Dhanji R. Prasanna 52cd19a015 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.
2026-01-15 13:49:29 +05:30

5.4 KiB

Prefer obvious, readable Racket over cleverness.

Control flow

;; 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

;; 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

;; 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 places for CPU parallelism, threads 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.