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.
5.4 KiB
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-definefor destructuring. - Use
cond,case,and,orcleanly; avoid nestedifpyramids. - Prefer
defineoverlet/let*when it reduces indentation. - Use
let*when bindings depend on earlier bindings; useletfor 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/foldfor complex accumulation;for/list,for/hashfor 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
providebeforerequire— interface at top. - Use
contract-outwhen correctness matters (public APIs, callbacks, data shapes). - Use explicit
providelists only — never(all-defined-out)in production. - Use
racket/basefor libraries (faster loading);racketfor scripts.
Contracts: when and how much
- Module boundaries: use
contract-outfor public APIs — catches bugs at the boundary with clear blame. - Internal functions: use
define/contractsparingly for tricky invariants or during debugging. - Higher-order contracts: use
->for simple functions;->iwhen you need dependent contracts. - In tests: contracts give fast feedback — keep them on during development, consider
#:unprotected-submodulefor 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, notref,free-spaces. - Use
-ref,-set,-updatesuffixes 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:
structvariants +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, orerrorwith a clear message. - Wrap IO and parsing with
with-handlersand 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-fileidiomatically.
Macros: use sparingly
- Don't write macros unless it meaningfully reduces boilerplate or enforces invariants.
- If writing macros: use
syntax-parse(not rawsyntax-case) and include good error messages. - Keep macro output readable and debuggable.
Phase separation
- Understand
for-syntaxvs runtime; don't accidentally pull runtime values into macros. - Use
begin-for-syntaxsparingly; prefersyntax-local-valuepatterns when possible.
Continuations: use sparingly
- Prefer
call/ec(escape continuations) over fullcall/ccwhen possible. - Use
parameterizefor 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
syncand events for composable waiting.
Gotchas
eq?vsequal?vseqv?: useequal?by default for structural comparison.null?only works on proper lists; useempty?fromracket/listfor generics.string=?notequal?for string comparison in hot paths.
Testing
- Use
module+ testsubmodules; run withraco test. - Add
rackunittests for tricky logic; prefer table-driventest-casewithcheck-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.