Racket module boundaries and contract enforcement: retroactive wrapping of unannotated exports
0 reputation · 28 Jun 2021, 11:21 UTC
0 reputation · 28 Jun 2021, 11:21 UTC
In Racket, the contract system is designed to enforce runtime checks on function inputs and outputs across module boundaries. A common integration scenario arises when one module exports a function without a contract-out annotation, and a downstream module wishes to enforce a stricter contract on that import without modifying the upstream source.
The standard provide and require forms support contract wrappers, but the documented API does not automatically wrap imported functions unless the importing module explicitly declares them. It remains unclear whether a supported pattern exists for the importer to retroactively apply a contract to an unannotated export, or whether such enforcement is strictly limited to the exporting module's interface. Behavior may differ between Racket 8.7 and newer releases, and unsafe modules in the dependency chain could bypass checks entirely.
Is there a documented provide/require pattern that allows an importing module to apply a contract to an unannotated function exported by another module? Does Racket 8.7 or later alter the default behavior so that imported functions without explicit contracts are automatically wrapped at the boundary? What is the supported approach to ensure contract enforcement remains intact when unsafe modules participate in the dependency chain?
35775 reputation · 28 Jun 2021, 20:15 UTC
Yes. Racket allows a module that imports an identifier to wrap it with a contract by using contract-out in the require clause. This does not modify the exporter’s interface; only the importer sees the contract.
(require (only-in mod foo)
(contract-out foo (-> Number Number)))
The contract is enforced the first time the imported function is called. It is a runtime check, so there is a small overhead.
Imported functions are not automatically wrapped. The contract must be declared explicitly either in the exporting module’s provide list or in the importer’s require clause. This has been stable since Racket 8.0 and remains unchanged in 8.7, 8.8, and later releases.
When an unsafe module sits between the exporter and the final consumer, it can re‑export the identifier without a contract, effectively bypassing checks imposed earlier. Two safe patterns exist:
contract-out at every boundary you control. Each module that re‑exports the identifier must add a contract-out in its require clause.contract-out, and re‑exports the wrapped value. All downstream modules then import this wrapper instead of the unsafe module.Example chain:
// alpha.rkt (unannotated export)
(provide foo)
(define (foo x) (+ x 1))
// safe-wrapper.rkt
(require (only-in alpha foo)
(contract-out foo (-> Number Number)))
(provide foo)
// consumer.rkt
(require safe-wrapper)
(foo 5) ; OK
(foo "a") ; contract violation
If the unsafe module re‑exports foo without a contract, callers of that module will not see the check unless the wrapper is applied again at the next boundary.
alpha.rkt with the unannotated function as shown above.safe-wrapper.rkt that uses contract-out and re‑exports foo.run.rkt file, require safe-wrapper and call (foo 5) – no error.(foo "a") – you should see a contract violation trace originating from safe-wrapper.rkt.racket -v consumer.rkt to see the full stack trace.contract-out in the require clause to retroactively apply a contract.contract-out at each boundary you control or provide a dedicated safe wrapper module.Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.