Best practices for contracts

Hello Racketeers,

lately I've encountered a few instances where I use contracts extensively to establish both intra- and inter- module boundaries. Some of the contracts are provided for other modules to use as well. I also use scribble/srcdoc extensively, which brings the following question:

What is the best way to define, provide and document a flat contract?

Minimal example would be a contract for values that specify line weight the way like they are specified in the Unicode standard. We can have light and heavy lines. So, without naming, the contract would look like:

(or/c false? 'light 'heavy)

Assuming we want to allow the user to leave the weight unspecified with #f value.

Reading the documentation has lead me to the following contracted contract specification (let's leave aside the fact whether contracting the contract makes sense right now):

(define/contract line-weight/c
  flat-contract?
  (flat-named-contract
   'line-weight/c
   (flat-contract-predicate
     (or/c false? 'light 'heavy))))

A bit heavy (self-pun intended), but it probably describes the intentions as clearly as possible. Invalid uses of contract would be caught and of course, contract violations would point directly at the offending code (assuming this is used correctly in all procedure contracts).

Now comes the tricky part. The best way to document and provide this contract using scribble/srcdoc I found is:

 (proc-doc/names
  line-weight/c
  (-> any/c boolean?)
  (v)
  @{

    Returns @racket[#t] if given value is valid @racket[line-style?]
    weight field value.

    Valid weights are:

    @itemlist[
              @item{@racket[#f] - no line}
              @item{@racket['light] - thin (light) line}
              @item{@racket['heavy] - thick (heavy) line}
              ]

    These weight names are taken from the Unicode standard Box Drawing
    block U+2500-U+257F.

    })

Yes, the (-> any/c boolean?) is more suited for predicates and actually is a bit misleading for a flat-contract? values. But it makes it clear that the contract can be (and is intended) to be used as predicate too.

Digging deeper in the documentation makes me think: false? or false/c or #f - which one to choose? Probably #f, after reading the history of false/c. But many core Racket modules use false? in contracts and so it feels natural to stick with that.

Another interesting situation is where the contract and predicate versions are supposed to match compatible yet different types. I typically want the contract to match any compatible value that can be canonically converted to the particular type and the predicate should return #t if and only if the type matches exactly.

Cheers,
Dominik

4 Likes

I use a mixture of A define/contract and B (provide (contract-out ...))
The former usually for code I am newly writing where I might have a module with a bunch of functions in it, as I add to the code and my understanding of what is needed, how data moves through the functions and what code can be grouped into logical units expands. I take the groups of functions and put them in their own modules, then I convert most A's to B's, only keeping A's when I haven't finished making sure that all module internal invariants are kept by the functions I have in that module.

Currently in my code I tend to just have:

(define line-weight/c (or/c false? 'light 'heavy))
(provide line-weight/c
         (contract-out [set-line-weight! (-> line-weight/c any/c)]))
(define (set-line-weight! lw) (void))

I think I will adopt using flat-named-contract that probably improves readability of error messages.
I wonder about flat-contract-predicate I think it can be left out a lot of times, to the point where I wasn't really aware of this function, I like that your example is so explicit (that would have been helpful to illustrate how contracts are "supposed" to be used when I started using them) makes it easier for learning. That said in practice I am leaving it out where it is possible.

The part I find most interesting about your example is putting a flat-contract? on your contract, while I think it is cool from a literal programming view point, to me it seems a bit too much on the runtime side.
But without actually digging into the contract implementation I can't be sure, I wonder whether define/contract will check that the contract is a flat-contract? only at run time, or whether there are some kinds of partial expansions that try to check some things about the expression at compile time.

If there isn't I would prefer something like a define/static/contract or define/meta/contract, which works sort of like asserts in imperative languages, you have a way to run the checks, but also a way to disable them, because the result doesn't change unless the literal expression does. Or alternatively the check always happens, but it happens at compile time (and errors for expressions that depend on runtime values). But I haven't thought enough about this, so its just an idea for now...

Regarding documentation, I haven't written enough documentation yet to have a clear opinions on this, from my limited experience I had a gut feeling of "something about writing documentation seems to clunky and labor intensive" which is one of the reasons I haven't written a lot of it, others are that I mostly write code that is more on the experimental side and I am the only one using it.
If I remember correctly Jay's remix language had some interesting ideas about making documentation easier to write but its documentation isn't showing up, because the package currently has a build failure. I think it had a define that could also be used to specify the documentation directly, but I could be wrong.

I am more used to seeing #f rather than false?, might be a matter of what you are more exposed to, I prefer #f because for me that is clearly a racket #f value, if I see false? I tend to think it might be a predicate for a domain specific notion of what is false, for some custom language for example.