It's important to be clear what contracts are for. Like a legal contract, Racket's contracts do two things:
- they tell all the parties to the contract what they must do to comply with the contract;
- they tell all the parties to the contract what they can assume as a result of the contract.
Both of these aspects are important, but the second one is more important here. As an example consider the factorial function:
(define (factorial n)
(if (= n 0)
1
(* n (factorial (- n 1)))))
This all seems to be fine:
> (factorial 10)
3628800
> (factorial 100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
Except it's not fine, at all:
> (factorial 12.2)
and there's a long pause and then at some point Racket runs out of memory.
Well, that's because when I wrote factorial
I wrote it with an understanding about what the domain and range of the factorial function are: for this version of the factorial function its domain is the natural numbers (integers greater than or equal to 0) and its range is integers greater or equal to 1. And the implementation relies critically on a property of natural numbers: a natural number n is either zero, or, if you subtract 1 from it enough times, that number is zero.
When I called factorial
I called it with a number which is not in its domain, but it didn't know that, so it simply failed to terminate. Well, we can solve this by providing a contract for factorial
. I'll do this by providing the contract directly on the function rather than at the module level, because it means I have to type less:
(define/contract (factorial n)
(-> natural-number/c (integer-in 1 #f))
(if (= n 0)
1
(* n (factorial (- n 1)))))
And now
> (factorial 10)
3628800
> (factorial 12.2)
; factorial: contract violation
; expected: natural-number/c
; given: 12.2
; [...]
Now there's a contract which protects factorial
from me. Unfortunately it also protects it from itself; each time it calls itself it has to dutifully check its own contract. Well, we can avoid that:
(define/contract (factorial n)
(-> natural-number/c (integer-in 1 #f))
(define (floop m r)
(if (= m 0)
r
(floop (- m 1) (* r m))))
(floop n 0))
within factorial
, I can assume things about the contract, and things don't need to be obsessively checked.
Except I made a mistake:
> (factorial 10)
; factorial: broke its own contract
; promised: exact-positive-integer?
; produced: 0
So now the contract on factorial
is protecting me from a buggy implementation. Code I write which uses factorial
can safely assume that its return value will be an integer greater or equal to 1. And I can fix that, of course:
(define/contract (factorial n)
(-> natural-number/c (integer-in 1 #f))
(define (floop m r)
(if (= m 0)
r
(floop (- m 1) (* r m))))
(floop n 1))
OK, so this has been a long preamble to say this:
- contracts place obligations on the parties to them;
- contracts allow the parties to them to make assumptions.
In particular, it is almost certainly the case that containers make assumptions that the objects being put into them are containees, and have the behaviour of containees, because they know that there is a contract in place which makes that be true.
If, somehow, you manage to circumvent that contract, then what will happen is that the container will still assume it's true, and still make the same assumptions. And the result will be disaster of some kind: if you're lucky you'll get errors, if you're not lucky there will just be trash on the screen or the program will crash (or, worst of all, not crash, but fill its memory with crap).
So the answer is, in brief: that contract exists for a reason, and if you want to place something into a container, you need to make sure it's a containee, and that it is not lying about being a containee – it needs to actually implement the behaviour of a containee properly, because (almost certainly) the container will be relying on that behaviour to work. That's what contracts are for.