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.