I've been going through Contracts in the Racket Guide.
The ->i
construct allows one to place arbitrary constraints on the input/output of a function.
For example, I could have an unzip
function that takes a list of pairs and returns two lists. Using contracts, I could confirm that every element of the in-list is a pair and that the out-lists have the corresponding elements.
The racket guide hints that this is when contracts are useful. But it seems like this would be better done inside the function itself. I could throw an error if I encounter a non-pair, this would check the in-list. The output is automatically checked by having a correct function.
What is a concrete example of where code is improved somehow by a contract more complex than simple types?
As you have described, pretty much any check that can be performed in ->i
can be performed within the function itself, but then again, any check performed by contracts can, for the most part, be performed within functions themselves. Encoding the information into a contract provides a few advantages.
These are most apparent with ->i
when the contract needs to specify dependencies within arguments supplied to the function. For example, I have a collections library, which includes a subsequence
function. It takes three arguments, a sequence, a start index, and an end index. This is the contract I use to protect it:
(->i ([seq sequence?]
[start exact-nonnegative-integer?]
[end (start) (and/c exact-nonnegative-integer? (>=/c start))])
[result sequence?])
This allows me to explicitly specify that the end index must be greater than or equal to the start index, and I don't have to worry about checking that invariant within my function. When I violate this contract, I get a nice error message:
> (subsequence '() 2 1)
subsequence: contract violation
expected: (and/c exact-nonnegative-integer? (>=/c 2))
given: 1
which isn't: (>=/c 2)
It can be used to ensure more complex invariants, too. I also define my own map
function, which, like Racket's built-in map
, supports variable numbers of arguments. The procedure supplied to map
must accept the same number of arguments as there are sequence provided. I use the following contract for map
:
(->i ([proc (seqs) (and/c (procedure-arity-includes/c (length seqs))
(unconstrained-domain-> any/c))])
#:rest [seqs (non-empty-listof sequence?)]
[result sequence?])
This contract ensures two things. First of all, the proc
argument must accept the same number of arguments as there are sequences, as mentioned above. Additionally, it also demands that that function always returns a single value, since Racket functions can return multiple values.
These invariants would be much harder to check inside the function body because, especially with the second invariant, they must be delayed until the function itself is applied. It must also be checked with every invocation of the function. Contracts, on the other hand, wrap the function and handle this automatically.
Do you always want to encode every single invariant of a function into a contract? Probably not. But if you want that extra level of control, ->i
is available.