A contract operates at run time and restricts the values that can pass across the boundary between two parts of a program. Contracts are primarily used with functions to check that input arguments and return values meet certain requirements. This makes it easier to prevent the consequences of bad input, and locate the source of the problem.
For instance, here’s an irresponsible our-div function that allows 0 as a denom argument. Bad input produces a generic error:
1 | /: division by zero |
We can write a check for that argument that gives more information about the nature of the error and where it’s coming from:
1 | our-div: denom argument needs to be nonzero |
Better still is a contract: it offers more compact & readable notation, more detail about the error, and takes care of all the underlying housekeeping. The contract below says that the first argument can be any number?, the second argument must be a number? that is not zero?, and the return value must be a number?:
1 2 3 4 5 6 7 8 9 10 11 12 13 | our-div: contract violation expected: (not/c zero?) given: 0 in: an and/c case of the 2nd argument of (-> number? (and/c number? (not/c zero?)) number?) contract from: (function our-div) blaming: anonymous-module (assuming the contract is correct) at: unsaved-editor:3.18 |
A function contract can be positioned at one of two boundaries (and in either place, we have to require racket/contract into the module):
At a module boundary, using contract-out with provide. In this case, only the calls to the function from outside the module will invoke the contract:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | (module divs br (require racket/contract) (provide (contract-out [int-div (integer? integer? . -> . integer?)])) (define (int-div num denom) (/ num denom)) (with-handlers ([exn:fail? (λ (e) 'inner-breach)]) (displayln (int-div 42 2.5)))) (require (submod "." divs)) (with-handlers ([exn:fail? (λ (e) 'outer-breach)]) (displayln (int-div 42 2.5))) |
1 2 | 16.8 ; no contract invoked from within the module 'outer-breach |
A module-boundary contract doesn’t have to live in the module where the function is defined. Here, the usual / function from br is re-exported with a more restrictive contract attached:
1 2 3 4 5 6 7 8 9 10 11 12 |
1 2 | 16.8 ; no contract invoked from within the module 'outer-breach |
At the function boundary, using define/contract. Every call to the function will trigger the contract, even within the module. Below, int-div has a contract for integer? input and output. When called with a floating-point argument, whether inside or outside the submodule, it will cause an error: + For demonstration purposes, we’re trapping these errors using with-handlers. See errors and exceptions.
1 2 3 4 5 6 7 8 9 10 11 12 13 | (module divs br (require racket/contract) (provide int-div) (define/contract (int-div num denom) (integer? integer? . -> . integer?) (/ num denom)) (with-handlers ([exn:fail? (λ (e) 'inner-breach)]) (displayln (int-div 42 2.5)))) (require (submod "." divs)) (with-handlers ([exn:fail? (λ (e) 'outer-breach)]) (displayln (int-div 42 2.5))) |
1 2 | 'inner-breach 'outer-breach |
More exotic combinations are possible. For instance, we could make a safe submodule for a given module that exports all the same bindings, but with contracts added. Then the user could choose whether to import that module with or without its contracts active, by choosing to require either the usual module-name or (submod module-name safe).
A function contract consists of a combinator followed by predicates—one predicate corresponding to each input argument, then one more corresponding to the return value. For a function with a fixed number of arguments (aka fixed arity), use the -> combinator:
This contract would be used with a function that takes two arguments—a string? and an integer?—and returns a number?. For readability, it’s conventional to write contracts using Racket’s infix notation, so this contract could be written like so:
Keyword arguments are handled by appending the keyword name to the contract predicate:
If the function has optional arguments, use the ->* combinator. The mandatory and optional arguments are grouped into their own parenthesized sublists. So a function with two mandatory string arguments and an optional integer argument would have this contract:
A contract that includes a rest argument must use the ->* combinator (because a rest argument is always optional) with the #:rest keyword:
A predicate is a function that takes any single value and returns a Boolean. Any predicate can be used within a contract—either predicates from the Racket library, or ones you create (use any/c in a contract to accept any value):
1 2 3 4 5 6 7 8 9 10 11 | (require racket/contract) (define/contract (divisible-by-3? x) (any/c . -> . boolean?) (zero? (modulo x 3))) (define/contract (f x) (divisible-by-3? . -> . integer?) x) (f 6) ; 6 (f 3) ; 3 (f 4) ; contract violation (not `divisible-by-3?`) |
1 2 3 4 5 6 7 8 9 10 11 | (require racket/contract) (define (divisible-by-3? x) (zero? (modulo x 3))) (define/contract (f x) ((and/c divisible-by-3? even?) . -> . (not/c flonum?)) x) (f 6) ; 6 (f 6.0) ; contract violation (not `(not/c flonum?)`) (f 3) ; contract violation (not `even?`) (f 4) ; contract violation (not `divisible-by-3?`) |
For lists and vectors, the contract library offers listof and vectorof, which check the members of these structures:
“If contracts are so great, why don’t we use them all the time?” Because they’re not free. + A Lisp programmer knows the value of everything, but the cost of nothing.
—Alan Perlis (channeling Oscar Wilde) A contract operates at run time, so it necessarily imposes a performance cost. Contracts offer the tradeoff—that we often encounter in programming—between safety and speed.
Contracts can impose unreasonable costs in three situations:
If a function is frequently called, then its contract will be too. The speed of that contract will have an outsize effect on the overall program. (Especially because it’s not uncommon to find that the contract takes more time than the rest of the function.)
Possible solution: write the argument checking by hand. Helper functions like raise-argument-error can create the same type of error a contract would (though slightly less descriptive):
1 2 3 | our-div: contract violation expected: nonzero number for denom given: 0 |
A contract is a function that calls a series of other functions to test its arguments. If those functions are themselves slow, then the contract will be too. Possible solution: simplify the contract to use faster checks.
If the data structures that pass through the contract boundary tend to be huge (like giant lists), any contract that checks every element (like listof) will be slow. Possible solution: package the data into a structure type, which can be tested quickly, because it has its own predicate.
Of course, any contract installed at a function boundary (with define/contract) will likely be called more often than one installed at a module boundary (with contract-out). Choosing boundaries wisely will improve performance by only using the contract where it’s needed most.
Following that logic, functions that touch the public interface of a program are likely to benefit more from contracts (because they have less control over their input) than functions that only touch the private interface.