Combining contract checking with normalization?

50 views
Skip to first unread message

Alexis King

unread,
Mar 6, 2022, 10:47:57 AM3/6/22
to Racket Users, Robby Findler, Christos Dimoulas

Hello,

As a user of the Racket contract system, I sometimes find myself thinking about the potential utility of “coercing” or “canonicalizing” contracts. In Racket programs, we often idiomatically allow values to be provided to a function in a non-canonical form for the sake of convenience. One example is the commonly-used path-string? contract, which is morally equivalent to using path? but allows the caller to omit an explicit use of string->path. Another example is the commonly-used failure-result/c contract, which allows the caller to omit wrapping non-procedures in a thunk.

While this idiom does make life easier for one party to the contract, it ultimately just transfers the burden of canonicalizing the value to the other party. This is unfortunate, because it results in a duplication of both logic and work:

  • Code to canonicalize the value must be written separately and kept in sync with the contract, which is error-prone.

  • The value ends up being inspected twice: once to determine if it satisfies the contract, and a second time to convert it to canonical form.

(In the nomenclature of a popular blog post I wrote a few years ago, these contracts are validating, not parsing.)

In theory, it is perfectly possible to implement a canonicalizing contract using the current contract system. However, such a contract has several practical downsides:

  • It is necessarily an impersonator contract, not a chaperone contract. This prevents its use in places that demand a chaperone contract, such as the key argument to hash/c.

  • It moves actual logic into the contract itself, which means using the uncontracted value directly is less convenient. This encourages placing the contract boundary close to the value’s definition to create a very small contracted region (e.g. via define/contract), even though blame is generally more useful when the contract boundary corresponds to a boundary between higher-level components (e.g. via contract-out).

  • There is no way to write such contracts using the combinators provided by racket/contract, so they must be implemented via the lower level make-contract/build-contract-property API. This can be subtle to use correctly, and it makes it unlikely that contract violations made by the contract itself will be blamed properly according to the “indy” blame semantics used by ->i.

All this is to say that the current contract system clearly discourages this use of contracts, which suggests this would be considered an abuse of the contract system. Nevertheless, the coupling between validating values and converting them to a normal form is so enormously tight that allowing them to be specified together remains incredibly compelling. I therefore have two questions:

  1. Has this notion of “canonicalizing” contracts been discussed before, whether in informal discussions or in literature?

  2. Is there any existing work that explores what adding such contracts to a Racket-style, higher-order contract system in a principled fashion might look like?

Thanks in advance,
Alexis

Robby Findler

unread,
Mar 6, 2022, 11:48:36 AM3/6/22
to Alexis King, Christos Dimoulas, Racket Users
I have certainly have thought that developing a library along these lines is a good idea for many years!

Robby

Matthias Felleisen

unread,
Mar 6, 2022, 12:08:58 PM3/6/22
to Robby Findler, Alexis King, Christos Dimoulas, Racket Users

p.s. I think implementing this via proxies is a bad idea not just for direct manipulation of the value or the incompatibility w/ some existing values (like numbers). A protocol between contracts and contracted functions is more what we need. 



--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/CAL3TdOMO3%2BEApMTyTmjxhzOZJ%2BUGE5P6h%3Dx1xTqDi7AgB3mggQ%40mail.gmail.com.

David Storrs

unread,
Mar 7, 2022, 10:56:52 AM3/7/22
to Alexis King, Racket Users, Robby Findler, Christos Dimoulas
Would this give part of what you're looking for?

#lang racket

(define (my-string-length s)
  ((or/c (and/c (or/c symbol? string?) (compose1 string-length ~a))
         (curry raise-arguments-error 'my-string-length "invalid arg" "arg"))
   s))

(my-string-length "foo")
(my-string-length 'foo)
(my-string-length 7)


It's not something you'd want to write by hand in a lot of places, but it seems an easy target for a macro.


--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.

jackh...@gmail.com

unread,
Mar 16, 2022, 9:17:21 PM3/16/22
to Racket Users
This is also something I want and have thought about for some time. My main use case is for functions that I want to accept an arbitrary sequence, but coerce the sequence to a list/set/etc. if it's not already a list/set/etc. I think one viable approach would be to do the following:
  1. Add a notion of "coercing contracts" whose expressiveness lies between chaperone contracts and impersonator contracts. A coercing contract is allowed to produce a non-equal result, but only in a way that "normalizes" the input: (c (c x)) should be equal to (c x) for any coercing contract c, but (c x) does not have to be equal to x. Many situations that currently disallow impersonator contracts could reasonably allow coercing contracts, including hash/c.
  2. Make it easier to combine define/contract and module boundary contracts. Using just define/contract gives you lousy blame boundaries for clients of your module, but that would be trivially easy to fix if `recontract-out` worked on identifiers bound with define/contract.
Using your path-string? example, this would result in something like this:

(provide (recontract-out read-config-file))
(define path-string/c (coerce/c path-string? string->path))
(define/contract (read-config-file path)
  (-> path-string/c config?)
  (parse-config (file->string path)))
Reply all
Reply to author
Forward
0 new messages