When is ->i actually useful in racket?
Asked Answered
Y

1

12

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?

Yellowweed answered 26/5, 2015 at 17:28 Comment(0)
J
13

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.

  1. 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.
  2. 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.

Joseph answered 26/5, 2015 at 17:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.