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

Reply via email to