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.
- You can extract the invariants out of the function implementation. This is nice because you don't need to clutter the function itself with guard clauses, you can just write code and know that invariants will be maintained by the contract.
- Contracts work hard to provide good error reporting. They'll automatically assign "blame" to the party that violates the contract, and for complex contracts, they'll add context to the error messages to make it as clear as possible what the problem is.
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.