[racket] contracts on liberal function inputs: avoiding duplicate effort

From: Matthew Butterick (mb at mbtype.com)
Date: Sat Feb 15 19:09:40 EST 2014

>
> Of course, these are not really contracts as we understand them. The docs
> for 'make-contract' say not to do this, if you read them carefully. So
> beware. I'm kind of curious what other people think of this application of
> the contract system, actually.


Why is it an abuse of the contract system to define a contract that uses a
projection to change the value?

The docs for make-contract say "The projection must either produce the
value, *suitably wrapped to enforce any higher-order aspects of the
contract*, or signal a contract violation." Is it never true that changing
a value can be the "suitable wrapping"? (Or m I overlooking some other
relevant part of the docs?)

Consider this contract. It checks whether a value can be converted to a
path. If so, it returns the converted value. If not, it fails. (This one's
in quasicode, but I wrote a real one and it works as expected.) If this is
abuse, it's very useful abuse.

(define coerce/path?
  (make-contract
   #:name 'coerce/path?
   #:projection (λ (b)
                  (λ (x)
                    (if (can-be-path? x)
                        (convert-to-path x)
                        (raise-blame-error
                         b x
                         '(expected: "~a" given: "~e")
                         'can-be-path? x))))))








On Mon, Nov 18, 2013 at 10:25 AM, Ryan Culpepper <ryanc at ccs.neu.edu> wrote:

> On 11/18/2013 12:03 PM, Matthew Butterick wrote:
>
>> In a function that permits liberal inputs, I often find that the input
>> processing I do in a contract is duplicated at the beginning of the body of
>> the function. Is this avoidable?
>>
>> Certain functions want to be liberal with input because there are
>> multiple common ways to represent the data. For instance, I have a function
>> that operates on CSS RGB colors. This function should be prepared to accept
>> these forms of input & understand that they're all the same:
>>
>> "#c00"
>> "#cc0000"
>> '("204" "0" "0")
>> #xcc0000
>> '(0.8 0 0)
>>
>> Let's say that my internal representation of an RGB color is described by
>> the contract rgb-color/c:
>>
>> (define rgb-color/c (list/c (real-in 0 1) (real-in 0 1) (real-in 0 1)))
>>
>> But I can't use rgb-color/c as the input contract for the function
>> because it's too narrow. So I make a second contract that tests for things
>> that can be converted to an rgb-color:
>>
>> (define (rgb-colorish? x) (or/c rgb-color/c [tests for the other input
>> formats ...] )
>>
>> To determine if the input is rgb-colorish?, this contract usually just
>> ends up trying to convert the input to rgb-color. If it works, then the
>> contract returns true.
>>
>> But after the contract returns, I have to convert the input to an
>> rgb-color anyhow. So I'm doing exactly the same work that the contract just
>> finished. If the conversion is expensive, I'm doing it twice.
>>
>
> We usually just don't worry about the conversion happening twice. Or we do
> what Matthias said. Or we apply a fast approximate contract (or none at
> all) and then do the conversion and error checking together inside the
> function.
>
> But... by abusing the contract system a little bit, though, we can get it
> to do the conversion for you.
>
> Here's a "contract" combinator that takes a conversion function that
> returns #f to indicate failure/rejection and any other value to represent
> the converted result.
>
> > (define (make-named-conversion-contract name convert)
>     (define ((proj blame) v)
>       (cond [(convert v )
>              => values]
>             [else
>              (raise-blame-error blame v
>                                 '(expected: "~a" given: "~e")
>                                 name v)]))
>     (make-contract #:name name #:projection proj))
>
> Here's a "contract" that checks that a value is real, and if so produces
> its absolute value.
>
> > (define abs/c
>     (make-named-conversion-contract
>      'abs/c
>      (lambda (v) (and (real? v) (abs v)))))
>
> And here's a function using the converting "contract":
>
> > (define/contract f (-> abs/c real?)
>     (lambda (x) x))
> > (f 10)
> 10
> > (f -12)        ;; <-- !!!
> 12
> > (f 'hello)
> f: contract violation
>  expected: abs/c
>  given: 'hello
>  ....
>
> Of course, these are not really contracts as we understand them. The docs
> for 'make-contract' say not to do this, if you read them carefully. So
> beware. I'm kind of curious what other people think of this application of
> the contract system, actually.
>
> Ryan
>
>
> ____________________
>  Racket Users list:
>  http://lists.racket-lang.org/users
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.racket-lang.org/users/archive/attachments/20140215/fd9d4620/attachment-0001.html>

Posted on the users mailing list.