Racket contracts are very cool, but can be very slow.
In the real world, a "term sheet" is a casual version of a contract.
This collection provides a lightweight alternative to full-on Racket
contracts. You get the declarative convenience of
but speed comparable to writing checks by hand. The speed is
typically 200x faster than
define/contract. (It's sometimes even
faster than handwritten checks that naively use predicate-creating
Plus, a runtime parameter allows even the fast checks to be skipped. This supports a work flow such as "debug vs. release" builds, albeit at run time not build time.
What's the catch? You lose the "blame" mechanism of contracts, and therefore the error messages are sometimes not as helpful
define/termsheet takes the same form as
define/contract, but uses
the contract predicates as inline checks. As a result, this runs much
faster -- e.g. ~200x faster -- than a normal, chaperoned wrapped
define/termsheet is source compatible with
define/contract, it should be possible to use it as a drop-in
replacement in existing code via
bench.rkt to see timings.
#t to benchmark the contract versions
(I have it set to
#f by default because they are so slow.)
With that enabled you may see results like this:
f/raw: cpu time: 9 real time: 9 gc time: 0 f/checked: cpu time: 10 real time: 10 gc time: 0 f/termsheet: cpu time: 17 real time: 18 gc time: 0 f/provide/contracted: cpu time: 433 real time: 435 gc time: 25 f/define/contracted: cpu time: 9167 real time: 9185 gc time: 157 f2/raw: cpu time: 30 real time: 30 gc time: 6 f2/checked: cpu time: 171 real time: 171 gc time: 6 f2/termsheet: cpu time: 71 real time: 71 gc time: 5 f2/provide/contracted: cpu time: 563 real time: 565 gc time: 17 f2/define/contracted: cpu time: 13502 real time: 13535 gc time: 265 f3/raw: cpu time: 10 real time: 10 gc time: 0 f3/checked: cpu time: 156 real time: 156 gc time: 0 f3/termsheet: cpu time: 55 real time: 55 gc time: 0 f3/provide/contracted: cpu time: 530 real time: 531 gc time: 15 f3/define/contracted: cpu time: 13529 real time: 13563 gc time: 267 f4/raw: cpu time: 18 real time: 17 gc time: 0 f4/termsheet: cpu time: 37 real time: 38 gc time: 0 f4/provide/contract: cpu time: 3186 real time: 3193 gc time: 63 f4/define/contract: cpu time: 16710 real time: 16750 gc time: 341 f5/raw: cpu time: 16 real time: 16 gc time: 0 f5/termsheet: cpu time: 219 real time: 220 gc time: 5 f5/provide/contract: cpu time: 669 real time: 672 gc time: 19 f5/define/contract: cpu time: 10378 real time: 10410 gc time: 179
Procedure argument contracts--worth it?
One question is how to handle contracts for arguments that are
procedures, such as the
(boolean? . -> . boolean?) in
(define/contract (foo a f) (boolean? (boolean? . -> . boolean?) . -> . any) (f a))
I experimented with a way that's much faster than real contracts, but
is still considerably slower than not checking at all: The
define/termsheet macro wraps these in an additional
lambda/termsheet. Although much faster than a real chaperon, it's
still quite slow compared to no checking.
A simpler approach is to transform procedure contracts into a simple
procedure? predicate, which is obviously vastly faster. Granted this
loses the deeper checking. However in my experience that's of limited
value, especially if the procedure being called already has its own
contract (or termsheet). It will check its own input args; checking
them twice is of no value. At most, it would be useful to check only
that the procedure returns what was expected. Even that is often not
necessary, since the calling function checks its return value.
Finally, the cheaper contracts are, the more likely other functions will use them. And my goal is to make termsheets really, really cheap.
As a result, I've settled on the approach of converting procedure
contracts into simple
This was perfect timing to apply some things I've learned while writing Fear of Macros.
It turned out to force me to really learn how to preserve lexical context in a macro.
It's also helping me start to appreciate more of what the full Racket contract system does, beyond my naive understanding ("it writes checks for you").
Perhaps it's possible for full Racket contracts to be faster for simple cases and obviate the practical need for anything like this.