I recently encountered a case where a contract that (as far as I can tell)
cannot be written using ->i can be easily expressed using ->d. This
surprised me, since I thought that ->i was strictly better than ->d (given
that the docs for ->d say, "This contract is here for backwards
compatibility; any new code should use ->i instead."). Specifically, using
->d I can express a mutual dependency between the arguments of a function
(an extended example follows), whereas ->i raises an error if "generation
of <something>'s contract depends on <something>'s value", even if
<something> depends on <something-else> and <something-else> depends on
<something>.

I wonder if the docs should reflect that there are situations when ->d has
advantages over ->i. (Or maybe there already is a way to do this with ->i,
but the limitations seem to be inherent to its guarantees.)

For a motivating example, consider an abstraction for pairs of functions
that convert values between an "encoded" representation and a "native"
representation:

> #lang racket
> (struct converter (encoded/c encoded->native native/c native->encoded)
> #:transparent)
> (define (converter/c #:extra-encoded/c [extra-encoded/c any/c]
>                      #:extra-native/c [extra-native/c any/c])
>   (rename-contract
>    (struct/dc converter
>               [encoded/c contract?]
>               [encoded->native
>                (encoded/c native/c)
>                (-> (and/c encoded/c extra-encoded/c)
>                    (and/c native/c extra-native/c))]
>               [native/c contract?]
>               [native->encoded
>                (encoded/c native/c)
>                (-> (and/c native/c extra-native/c)
>                    (and/c encoded/c extra-encoded/c))])
>    `(converter/c #:extra-encoded/c ,(contract-name extra-encoded/c)
>                  #:extra-native/c ,(contract-name extra-native/c))))
> (define/contract (converter-decode c encoded)
>   (->i ([c (converter/c)]
>         [encoded (c) (converter-encoded/c c)])
>        [_ (c) (converter-native/c c)])
>   ((converter-encoded->native c) encoded))
> (define/contract (converter-encode c native)
>   (->i ([c (converter/c)]
>         [native (c) (converter-native/c c)])
>        [_ (c) (converter-encoded/c c)])
>   ((converter-native->encoded c) native))


A nice addition would be an operation to compose converters, constructing a
converter from the encoded representation of converter A to the native
representation of converter B:

> (define unsafe-compose-converters
>   (match-lambda**
>       [{(converter a:encoded/c a:encoded->native a:native/c
> a:native->encoded)
>         (converter b:encoded/c b:encoded->native b:native/c
> b:native->encoded)}
>        (converter a:encoded/c
>                   (compose1 b:encoded->native a:encoded->native)
>                   b:native/c
>                   (compose1 a:native->encoded b:native->encoded))]))


The next step is to protect this operation with a contract requiring that
the two converters use a compatible intermediate representation.
Specifically, we want to insist that the encoded->native function of
converter A must always produce values satisfying the encoded/c contract of
converter B and that the native->encoded function of converter B must
always produce values satisfying the native/c contract of converter A. This
is easily done using ->d (though note that we must explicitly test that
both arguments are converters before using them in the contract, as ->d,
unlike ->i, does not guarantee this):

> (define/contract compose-converters
>   (->d ([encoded-to-intermediate (converter/c #:extra-native/c
>                                               (if (converter?
> intermediate-to-native)
>                                                   (converter-encoded/c
> intermediate-to-native)
>                                                   any/c))]
>         [intermediate-to-native (converter/c #:extra-encoded/c
>                                              (if (converter?
> encoded-to-intermediate)
>                                                  (converter-native/c
> encoded-to-intermediate)
>                                                  any/c))])
>        [_ (if (and (converter? encoded-to-intermediate)
>                    (converter? intermediate-to-native))
>               (converter/c #:extra-encoded/c (converter-encoded/c
> encoded-to-intermediate)
>                            #:extra-native/c (converter-native/c
> intermediate-to-native))
>               none/c)])
>   unsafe-compose-converters)


Now, if we compose two incompatible converters and try to use the result:

> (define converter:string->number/false
>   (converter string?
>              string->number
>              (or/c number? #f)
>              (λ (native)
>                (if native
>                    (number->string native)
>                    ""))))
> (define converter:number->number
>   (converter number? add1 number? sub1))
> (converter-decode (compose-converters converter:string->number/false
>                                       converter:number->number)
>                   "")


we get an error blaming the user of compose-converters for the #f produced
by the encoded->native function of converter:string->number/false.

Why can't we use ->i? With ->i, either the contract for the first argument
can depend on the value of the second argument or visa versa, but the two
contracts can't each depend on the other value (and neither argument's
contract can depend on that same argument's value).

In this case, insisting on compliance from one side is not enough. If we
try to use ->i:

> (define/contract buggy-compose-converters
>   (->i ([encoded-to-intermediate (converter/c)] ; can't have mutual
> dependency
>         [intermediate-to-native (encoded-to-intermediate)
>                                 (converter/c #:extra-encoded/c
>                                              (converter-native/c
> encoded-to-intermediate))])
>        [_ (encoded-to-intermediate intermediate-to-native)
>           (converter/c #:extra-encoded/c (converter-encoded/c
> encoded-to-intermediate)
>                        #:extra-native/c (converter-native/c
> intermediate-to-native))])
>   unsafe-compose-converters)
> (converter-decode (buggy-compose-converters converter:string->number/false
>                                             converter:number->number)
>                   "")


the error blames (definition buggy-compose-converters) for calling the
encoded->native function of converter:number->number with #f.

It seems like the prohibition on mutual- or self- dependency is an
unavoidable consequence of ->i enforcing contracts internally: it obviously
can't guarantee that some value satisfies its contract while evaluating the
code that produces the contract. In this case, though, we want to enforce
contracts on the arguments that are stronger (in the sense
of contract-stronger?) than the contracts protecting our contract-creation
code (which in the case of ->d is effectively any/c, though really we would
like it to be something like converter? to replace the manual checks). In
other words, if we think of ->i as creating two boundaries, we want to
enforce stronger contracts at one boundary than at the other.

Perhaps there should be some sort of ->i* form that makes the two
boundaries explicit?

Regardless, I think some revision of the docs for ->d would be in order: I
tried all sorts of things before I even thought to look at it, because my
previous impression was that ->i could do everything ->d can do.

-Philip

-- 
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+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to