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?)]) and define/contract reject a wrong call at the boundary and name the module at fault
  • ⚠️ #lang typed/racket types part of a program: A typed module imports an untyped one through require/typed and a contract, so static and dynamic code meet at a checked boundary

1.1.2 Construction

  • ✅ struct declares 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-syntax form 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
  • ✅ #lang fixes 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, and with-handlers covers the exceptional path
  • ✅ A place collects its own heap: A place runs 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 the PLTSTDERR collection 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-port or 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-custodian and custodian-shutdown-all shut 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, and raise-user-error are not declared in a signature, so a caller cannot see the failure set from the definition
  • ✅ with-handlers selects 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 of exn:fail make a failure an inspectable value
  • ✅ A failure can be a value instead of a throw: A struct with an ok and an error variant, or a pair of values, carries the outcome without an exception
  • ✅ A contract violation blames a party: An exn:fail:contract carries 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 with dynamic-wind rather than by the handler

1.1.5 Concurrency

  • ✅ A place gives shared-nothing parallelism: place runs a computation on its own thread and store, and place-channel-put and place-channel-get pass 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, and channel-put and channel-get pass values between threads
  • ✅ Non-blocking I/O composes through events: sync on a port or on an async-channel waits 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 box or a vector written from two threads needs a semaphore or box-cas!, and the runtime reports no race

1.1.6 Platform

  • ✅ A program compiles to a native executable: raco exe produces 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
  • ✅ raco covers install, build, and delivery: raco pkg, raco exe, and raco distribute are 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 distribute is 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
  • #lang fixes 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, and cdr model 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/cc and dynamic-wind save 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 pkg is a first-class package manager: raco pkg install takes 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, and racket/gui come 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
  • ✅ #lang isolates a change: A DSL fixes the language of its own files, so a change to the base language does not reach a #lang that 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/bsl are built for a first course
  • ✅ Good for compilers and interpreters: #lang plus raco makes a language implementation a package, and the Beautiful Racket book builds several
  • ✅ racket/gui covers a desktop application: The GUI toolkit and racket/draw ship 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 #lang differ 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 let or define, so no new grammar is needed
  • ✅ A reader extension can change the surface: #lang plus 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

  • ✅ rackunit ships with the language: check-equal?, check-true, and test-case are in the distribution, and raco test runs 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/sandbox runs untrusted code with limits: A sandboxed evaluator carries its own language, a memory limit, and a custodian that bounds its run time
  • ✅ eval is explicit and scoped: eval and dynamic-require are visible calls, and the namespace decides what the evaluated code can reach
  • ⚠️ eval on a term built from input is the injection pattern: (eval (read input)) executes the text, and read itself 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?

  • ⚠️ Runs on a virtual machine, above native code in the energy and time benchmarks [5]; a causal re-analysis attributes most of the gap to execution time [6].

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:

All published papers

References

[1]
Y. Loth, “On the nature of coupling: All coupling as shared resource access.” Zenodo, Aug. 2026. doi: 10.5281/zenodo.21925122.
[2]
Y. Loth, “Coupling as a consequence of cohesion: Decomposition, the coupling floor, and domain-imposed coupling.” Zenodo, Aug. 2026. doi: 10.5281/zenodo.21925201.
[3]
Y. Loth, “On the nature of cohesion: Cohesion as correctness relative to a partitioning rule.” Zenodo, Jul. 2026. doi: 10.5281/zenodo.21703193.
[4]
M. Flatt and PLT, “The racket reference.” Racket, 2024. Available: https://docs.racket-lang.org/reference/
[5]
R. Pereira et al., “Energy efficiency across programming languages: How do energy, time, and memory relate?” in Proceedings of the 10th ACM SIGPLAN international conference on software language engineering (SLE), 2017, pp. 256–267. doi: 10.1145/3136014.3136031.
[6]
N. van Kempen, H.-J. Kwon, D. T. Nguyen, and E. D. Berger, “It’s not easy being green: On the energy efficiency of programming languages,” in Proceedings of the 40th IEEE/ACM international conference on automated software engineering (ASE), 2025. doi: 10.1109/ASE63991.2025.00131.
[7]
S. Joel, J. J. Wu, and F. H. Fard, “A survey on LLM-based code generation for low-resource and domain-specific programming languages,” ACM Transactions on Software Engineering and Methodology, 2025, doi: 10.1145/3770084.