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).
6.3 KiB
6.3 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.
Performance
- Use
in-list,in-vector,in-hashexplicitly inforloops — faster than generic sequence. - Beware
list-refin a loop — it's O(n) per call, so O(n²) overall. Use vectors for indexed access. - Don't repeatedly
appendin loops; usefor/listor accumulate withconsthenreverse. - Prefer vectors for indexed access, hashes for keyed lookup, lists for sequential iteration.
- Use
for/foldto build results in one pass instead of multiple traversals.
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.
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
parameterizescope 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-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
- Prefer
call/ec(escape continuations) over fullcall/cc— simpler, faster, sufficient for early exit. - Don't use continuations for what
parameterizeor exceptions handle better.
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.