Racket (8.17)
This assessment applies the coupling account — coupling as access to a shared resource, and the coupling floor [1], [2] — and the knowledge-embodiment theorem [3]; the language is assessed against its official reference [4].
1 Assessment
This page assesses Racket in five parts: core coupling — what the language’s own constructs force; ecosystem coupling — what the surrounding platform, tooling, and community cost; engineering readiness — whether the artifact can be reproduced, verified, observed, worked in quickly, connected to other languages, and governed openly; the profile — the unscored facts a score cannot express; and the scoring detail — the mechanism marks and the arithmetic behind every score. A ✅ bullet marks a construct that helps. A ⚠️ or ❌ bullet marks a cost; the scoring model does not price every cost named here.
Core coupling 62/100 · Ecosystem coupling 84/100 · Engineering readiness 57/100
1.1 Core coupling
1.1.1 Type
- ❌ Typing is dynamic with no static check (T1): A value carries its type at run time, so a wrong-typed use fails when the expression executes
- ❌ Dispatch is resolved at run time (T3):
(send obj 'msg)and a generic method resolve when called, so a missing method fails there rather than at compile time - ❌ There are no sum types and no core generics (T4, T5): An alternative is a struct, a symbol, or a tagged list plus a contract, and no type parameter exists in the core language
- ❌ The numeric operators dispatch on a hidden type (T8):
+,*, and=accept any number, and the exact or the inexact result follows the values at run time, so the arithmetic is overloaded without a declared type - ✅ A contract checks the module boundary at run time:
(provide/contract [f (-> integer? integer?)])anddefine/contractreject a wrong call at the boundary and name the module at fault - ⚠️
#lang typed/rackettypes part of a program: A typed module imports an untyped one throughrequire/typedand a contract, so static and dynamic code meet at a checked boundary
1.1.2 Construction
- ✅
structdeclares the constructor with its invariant:(struct posn (x y) #:transparent #:guard check)creates the constructor, the predicate, and the accessors together, and the guard rejects a bad argument - ✅ A macro extends the language at compile time: A
define-syntaxform sees its syntax objects and expands to core forms, so a construct is added without a run-time library - ✅ Hygiene prevents accidental name capture: A binding the macro introduces cannot collide with a name at the use site, so the expansion does not depend on the caller’s names
- ✅ A module scopes what it defines: A binding is private until it is
provided, so a helper constructor stays invisible outside the module - ✅
#langfixes the whole surface of a file: A module states its language on the first line, so a DSL and the base language do not mix by accident - ⚠️
set!reaches any binding in the core language: A variable can be reassigned unless the language of the file forbids it, so immutability is not enforced by the core
1.1.3 Memory
- ✅ A
call-with-*form brackets a resource:(call-with-input-file path proc)opens and closes around the call, andwith-handlerscovers the exceptional path - ✅ A place collects its own heap: A
placeruns on a separate thread with a separate store, so parallel work does not extend one collection pause - ❌ Collection is non-deterministic (M2): The collector runs on its own schedule, and
(collect-garbage)or thePLTSTDERRcollection log is the only control a program has - ❌ There is no RAII (M3): There is no scope-bound destructor, so a port, a TCP listener, or a subprocess is released by an explicit
close-input-portor by a bracket the author writes - ❌ There is no ownership model (M4): No static lifetime or aliasing rule ties a value to an owner, so a shared mutable structure escapes its module without a trace
- ⚠️ A custodian groups resources only when it is created:
make-custodianandcustodian-shutdown-allshut down a group of threads and ports together, and the code must ask for that grouping
1.1.4 Error
- ❌ Exceptions are unchecked (Er2):
raise,error, andraise-user-errorare not declared in a signature, so a caller cannot see the failure set from the definition - ✅
with-handlersselects the recovery by predicate:(with-handlers ([(lambda (e) (exn:fail? e)) handler]) body)chooses the handler from the exception’s type rather than from a code number - ✅ An exception is a struct with fields:
(exn-message e),(exn:fail:contract? e), and the continuation marks ofexn:failmake a failure an inspectable value - ✅ A failure can be a value instead of a throw: A struct with an
okand anerrorvariant, or a pair of values, carries the outcome without an exception - ✅ A contract violation blames a party: An
exn:fail:contractcarries the offending value, the contract, and the module blamed, so a boundary fault is localised - ⚠️ A catch-all handler hides the cause:
(with-handlers ([void void]) ...)discards the reason, and any cleanup must be arranged withdynamic-windrather than by the handler
1.1.5 Concurrency
- ✅ A place gives shared-nothing parallelism:
placeruns a computation on its own thread and store, andplace-channel-putandplace-channel-getpass values, so several cores are used without shared memory - ✅ A future parallelises an expression:
(future expr)runs on a thread from the pool and(touch f)forces the result, and the runtime keeps a future’s computation safe on its own - ✅ A thread is a lightweight green thread:
(thread proc)starts one,(sync evt)waits on an event, andchannel-putandchannel-getpass values between threads - ✅ Non-blocking I/O composes through events:
syncon a port or on anasync-channelwaits for whichever is ready, so no callback pyramid is needed - ✅ A custodian bounds a thread group: A supervisor custodian shuts down its threads and ports together, so a group of tasks is torn down as a unit
- ⚠️ A shared box still needs a lock: A
boxor avectorwritten from two threads needs asemaphoreorbox-cas!, and the runtime reports no race
1.1.6 Platform
- ✅ A program compiles to a native executable:
raco exeproduces a standalone binary and bundles the runtime and the libraries it needs - ✅ The runtime runs on the major platforms: Windows, macOS, Linux, and the BSDs are supported from one source
- ✅ Racket CS builds on Chez Scheme: The default runtime compiles with Chez’s compiler, and the earlier bytecode runtime with its JIT remained available as the alternative
- ✅
racocovers install, build, and delivery:raco pkg,raco exe, andraco distributeare one tool family for a project’s whole lifecycle - ⚠️ The runtime must be present unless it is bundled: A source program needs an installed Racket, and
raco distributeis the step that removes that dependency - ⚠️ Cross-compilation is limited: An executable targets the platform that built it unless a cross build is arranged
1.1.7 Trade-offs (not coupling)
- Language-oriented programming is the premise: Building a DSL is the design centre of the language, and the work of designing the notation is the price of a task-specific language
- Contracts are opt-in: A module can leave a boundary unchecked, and the per-call cost of a contract follows from asking for the check
#langfixes the language for a whole file: A module cannot change its language halfway, which is a deliberate uniformity rather than a coupling defect- A macro can obscure its expansion: A macro is powerful, and the readability cost of an unusual expansion is a style decision
- A linked list is the core sequence type:
cons,car, andcdrmodel a sequence as a pair chain, so the cost in memory locality is a representation choice - The λ-calculus core is deliberately small: The core language is tiny because a macro expands to it, which is a design decision
1.2 Ecosystem coupling
1.2.1 Performance
- ✅ Racket CS compiles through Chez Scheme: A module is compiled to native code ahead of time, so an interpreted step is not in the hot path
- ✅ Hot code is compiled rather than interpreted: The compiler and the JIT of the runtime remove repeated interpretation of a loop
- ✅ A future and a place use several cores: A computation split across futures runs on the available threads and places
- ⚠️ Numeric code is moderate: A boxed number and a generic arithmetic operation over the numeric tower cost more than unboxed arithmetic in a compiled language
- ⚠️ A contract adds a check per boundary crossing: A contract on a hot function is a run-time check on every call
- ⚠️ A continuation capture does real work:
call/ccanddynamic-windsave and restore the control state, so a control abstraction is not free
1.2.2 Ecosystem
- ❌ The package ecosystem is small (Ec3): The count of published packages is well below a mainstream language’s, so a project may write what it cannot find
- ✅
raco pkgis a first-class package manager:raco pkg installtakes a package name, a file, or a Git URL, so a dependency needs no manual vendoring step - ✅ The distribution ships a large library:
racket/base,racket/list,racket/contract,racket/class,racket/draw, andracket/guicome in one installation - ✅ DrRacket is an IDE for both beginner and expert: The editor, the REPL, and the stepper are part of the distribution, and the stepper shows one reduction at a time
- ✅ Scribble documents a package in the language itself: Scribble writes prose with Racket syntax and produces HTML, PDF, and the reference pages from one source
- ⚠️ The community is small: Questions are answered, but by fewer people than in a mainstream ecosystem
1.2.3 Evolution
- ✅ The language is stable and backward compatible: Racket 8.x runs code written for earlier releases, and the compatibility promise covers the core
- ✅
#langisolates a change: A DSL fixes the language of its own files, so a change to the base language does not reach a#langthat pins it - ✅ The change of runtime kept the language: Racket CS replaced the bytecode runtime beneath the language, and existing source recompiled
- ✅ A package can be held at a version: An installation can be pinned to a version or to a repository revision, so a build can be made reproducible
- ⚠️ A library API can change between releases: The core is stable, and a library outside it may change, so an upgrade needs a test run
1.2.4 Domain
- ✅ Excellent fit for language research and DSLs: Macros,
#lang, and the reader produce a new language from a module, as the paper “Languages as Libraries” documents - ✅ Strong fit for teaching: DrRacket, How to Design Programs, and the teaching languages such as
#lang htdp/bslare built for a first course - ✅ Good for compilers and interpreters:
#langplusracomakes a language implementation a package, and the Beautiful Racket book builds several - ✅
racket/guicovers a desktop application: The GUI toolkit andracket/drawship in the distribution, so a windowed program needs no external dependency - ⚠️ Commercial adoption is small with notable exceptions: Naughty Dog’s game scripting language and the Arc dialect behind Hacker News run on Racket, but the general production footprint is small
- ⚠️ Heavy numeric work belongs in a library: Array and tensor computation depends on a package rather than on the core language
1.2.5 Learning
- ❌ The concepts are many even when the syntax is small (L1): Macros, continuations, modules, and contracts are separate topics, and each takes practice
- ❌ A macro is a topic of its own: Writing a macro requires the syntax-object model and the phase distinctions, which are separate from ordinary programming
- ✅ DrRacket and the stepper make evaluation visible: A beginner steps an expression and sees each reduction of the program
- ✅ A complete curriculum exists: How to Design Programs and its teaching languages are free and aligned with the IDE
- ✅ The documentation is comprehensive: The Racket Reference and the Racket Guide cover the language and each library with examples
- ⚠️ A Scheme background helps and misleads: The parentheses and the λ-calculus core transfer, while the module system, structs, and
#langdiffer from a bare Scheme
1.2.6 Syntax
- ✅ The syntax is uniform: Every form is a parenthesised list, so a reader learns one shape and applies it everywhere
- ✅ A user form looks like a built-in form: A macro-defined form has the same shape as
letordefine, so no new grammar is needed - ✅ A reader extension can change the surface:
#langplus a readtable defines a different notation for one file without changing the language - ✅ Prefix notation removes precedence rules:
(+ (* a b) c)states the order of operations in its nesting, so no precedence table must be learned - ⚠️ The parentheses are dense: A deeply nested expression needs careful counting, and most editors use a paredit or rainbow-paren mode to help
- ⚠️ A quoted form blurs data and code:
'(a b)is a list and(a b)is a call, so one character changes the meaning of a form
1.2.7 Testing
- ✅
rackunitships with the language:check-equal?,check-true, andtest-caseare in the distribution, andraco testruns a directory of tests - ✅ A test can live in the module it tests: A
(module+ test ...)submodule holds the tests, so no separate test file is needed - ✅ A contract is an executable invariant: A contract on an exported function rejects a wrong call, so a test need not re-check the boundary
- ✅ A failure reports both values: A failed
check-equal?prints the expected value, the actual value, and the source location - ✅ The debugger and the stepper inspect a real run: DrRacket’s debugger steps the evaluation, and the stepper shows the reduction sequence
- ⚠️ Dynamic typing defers a fault to the executed path: A wrong-typed call passes the build and fails a run, so the covered paths matter
1.2.8 Security
- ✅ The runtime is memory-safe: No pointer arithmetic and no manual free exist, so a use-after-free or a buffer overflow cannot be written in Racket source
- ✅ A contract guards a trust boundary: A contract on a value that arrives from outside rejects a wrong shape at the boundary and names the party at fault
- ✅
racket/sandboxruns untrusted code with limits: A sandboxed evaluator carries its own language, a memory limit, and a custodian that bounds its run time - ✅
evalis explicit and scoped:evalanddynamic-requireare visible calls, and the namespace decides what the evaluated code can reach - ⚠️
evalon a term built from input is the injection pattern:(eval (read input))executes the text, andreaditself can allocate on a crafted input - ⚠️ Two places share the file system: A place shares no memory with another, but two places can still corrupt one file, which the language does not prevent
1.3 Engineering readiness
1.3.1 Reproducibility
- ❌ Builds are not byte-reproducible by default.
- ❌ Dependencies resolve without a lock by default.
1.3.2 Supply chain
- ⚠️ Published packages carry no signed provenance by default.
- ⚠️ The package registry is a flat name space.
1.3.3 Observability
- ⚠️ No stable tracing and metrics standard.
- ⚠️ The standard library has no structured logging.
1.3.4 Feedback
- ✅ No charged mechanism in this dimension.
1.3.5 Interop
- ⚠️ No maintained portable target.
1.3.6 Governance
- ✅ No charged mechanism in this dimension.
1.4 Profile
Unscored, and kept separate from every score on purpose. The profile records what a mechanism price cannot express: what a running program costs, how well a coding model and an agent work in the language, and whether a safety-critical path exists.
1.4.1 Energy and cost
What this axis asks: what does a running program cost in energy and time?
1.4.2 AI and agent readiness
What this axis asks: how well does a coding model and an agent work in this language?
- ❌ Sparse training data: models write it unreliably [7].
1.4.3 Safety certification
What this axis asks: is there a certified or standards-qualified path to a safety-critical system?
- ⚠️ No certified or standards-qualified path for safety-critical systems.
1.5 Scoring detail
1.5.1 Core coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Type | T1 |
dynamic no static check | ✅ | −15 |
T2 |
weak static | — | 0 | |
T3 |
dynamic/duck dispatch | ✅ | −8 | |
T4 |
no sum types | ✅ | −4 | |
T5 |
no generics | ✅ | −4 | |
T6 |
no namespaces | — | 0 | |
T7 |
type erasure/reflection | — | 0 | |
T8 |
operator overloading | ✅ | −4 | |
T9 |
inheritance required for polymorphism | — | 0 | |
T10 |
no null-safety | — | 0 | |
T11 |
no module boundary | — | 0 | |
T12 |
no effect tracking | ✅ | −8 | |
T13 |
async/sync colouring | — | 0 | |
T14 |
unchecked foreign boundary | ✅ | −4 | |
| Type subtotal | −15 (−47) | |||
| Construction | C1 |
global mutable state by default | — | 0 |
C2 |
no immutability by default | — | 0 | |
C3 |
constructors coupled to allocation | — | 0 | |
C4 |
no constructor/validation | — | 0 | |
| Construction subtotal | 0 | |||
| Memory | M1 |
manual deallocation | — | 0 |
M2 |
GC non-deterministic pauses | ✅ | −8 | |
M3 |
no RAII | ✅ | −4 | |
M4 |
no ownership model | ✅ | −8 | |
| Memory subtotal | −15 (−20) | |||
| Error | Er1 |
no structured path | — | 0 |
Er2 |
unchecked exceptions only | ✅ | −8 | |
Er3 |
errors ignored/non-exhaustive | — | 0 | |
Er4 |
no composable result type | — | 0 | |
| Error subtotal | −8 | |||
| Concurrency | K1 |
GIL/no parallel | — | 0 |
K2 |
shared mutable state default | — | 0 | |
K3 |
manual sync no race detection | — | 0 | |
K4 |
no structured concurrency | — | 0 | |
K5 |
no primitives | — | 0 | |
| Concurrency subtotal | 0 | |||
| Platform | P1 |
cannot function outside one vendor/domain | — | 0 |
P2 |
single-vendor but other uses | — | 0 | |
P3 |
legacy/discontinued platform | — | 0 | |
| Platform subtotal | 0 |
Core Dimensions score: 62/100 — total cost = −38 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.2 Ecosystem coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Performance | R1 |
interpreted no AOT/JIT | — | 0 |
R2 |
GC pauses | — | 0 | |
R3 |
JIT warmup | — | 0 | |
| Performance subtotal | 0 | |||
| Ecosystem | Ec1 |
no package manager | — | 0 |
Ec2 |
fragmented tooling | — | 0 | |
Ec3 |
small/niche/discontinued | ✅ | −8 | |
Ec4 |
proprietary | — | 0 | |
Ec5 |
platform-locked tooling | — | 0 | |
Ec6 |
monoculture | — | 0 | |
Ec7 |
legacy burden | — | 0 | |
| Ecosystem subtotal | −8 | |||
| Evolution | V1 |
breaking changes | — | 0 |
V2 |
frozen | — | 0 | |
V3 |
discontinued | — | 0 | |
V4 |
glacial standards | — | 0 | |
| Evolution subtotal | 0 | |||
| Domain | D1 |
domain/vendor lock-in | — | 0 |
D2 |
poor domain fit | — | 0 | |
D3 |
framework forces architecture | — | 0 | |
| Domain subtotal | 0 | |||
| Learning | L1 |
steep | ✅ | −8 |
L2 |
large surface/multi-paradigm | — | 0 | |
L3 |
sparse docs | — | 0 | |
| Learning subtotal | −8 | |||
| Syntax | S1 |
verbose | — | 0 |
S2 |
inconsistent | — | 0 | |
S3 |
punctuation-heavy | — | 0 | |
| Syntax subtotal | 0 | |||
| Testing | G1 |
weak tooling | — | 0 |
G2 |
poor debugging | — | 0 | |
G3 |
runtime-only errors | — | 0 | |
| Testing subtotal | 0 | |||
| Security | Q1 |
memory-unsafe | — | 0 |
Q2 |
dynamic/weak typing risk | — | 0 | |
Q3 |
known vulnerability/injection | — | 0 | |
| Security subtotal | 0 |
Meta Dimensions score: 84/100 — total cost = −16 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.3 Engineering readiness Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Reproducibility | Y1 |
build not reproducible | ✅ | −8 |
Y2 |
dependencies not pinned | ✅ | −8 | |
| Reproducibility subtotal | −15 (−16) | |||
| Supply chain | Y3 |
no signed provenance | ✅ | −8 |
Y4 |
registry without namespacing | ✅ | −4 | |
| Supply chain subtotal | −12 | |||
| Observability | Y5 |
no stable tracing standard | ✅ | −8 |
Y6 |
no structured logging | ✅ | −4 | |
| Observability subtotal | −12 | |||
| Feedback | Y7 |
slow build | — | 0 |
Y8 |
no fast in-editor analysis | — | 0 | |
| Feedback subtotal | 0 | |||
| Interop | Y9 |
no foreign-function interface | — | 0 |
Y10 |
no portable target | ✅ | −4 | |
| Interop subtotal | −4 | |||
| Governance | Y11 |
single-vendor governance | — | 0 |
Y12 |
closed specification | — | 0 | |
| Governance subtotal | 0 |
Readiness Dimensions score: 57/100 — total cost = −43 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
Verdict: Racket’s design yields moderate core coupling (62/100) and better platform and ecosystem coupling (84/100). The language-oriented model is a real decoupling device: a macro expands to core forms without a name from the use site leaking in, a struct carries its own guard, a module hides what it does not provide, and a contract checks a boundary and names the party at fault. The cost sits in the type system and the heap, where typing is dynamic, dispatch resolves at run time, there are no sum types or core generics, and memory has neither an ownership rule nor a block-scoped release, with collection decided by the runtime. Around the language, the package ecosystem is small and the concept load is heavy, while the toolchain, the documentation, and the debugging support are complete.
The formal development. The definitions, theorems, and proofs behind this page are published in:
- On the Nature of Coupling: All Coupling as Shared Resource Access — Zenodo, 2026 — doi:10.5281/zenodo.21925122
- Coupling as a Consequence of Cohesion: Decomposition, the Coupling Floor, and Domain-Imposed Coupling — Zenodo, 2026 — doi:10.5281/zenodo.21925201
- On the Nature of Cohesion: Cohesion as Correctness Relative to a Partitioning Rule — Zenodo, 2026 — doi:10.5281/zenodo.21703193