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?
DrRacket IDE The editor provides highlighting for syntax and run-time errors, parenthesis matching, a debugger and an algebraic stepper.
So it is easy to learn, because there is a lot of documentation, but it is also hard, because you need to actually read it. You cannot pick up Racket in a few weekends. The good news is that some people will find that exciting, a perfect chance to learn a new family of languages.
expt : (number number -> number) Purpose: to compute the power of the first to the second number. floor : (real -> integer) Purpose: to determine the closest integer (exact or inexact) below a real number.
While Racket originated in the functional world of programming languages, it also is a full-fledged class-based, object-oriented programming language.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With