[racket-dev] [plt] Push #24906: master branch updated

From: Stevie Strickland (sstrickl at ccs.neu.edu)
Date: Tue Jun 26 08:07:58 EDT 2012

On Jun 26, 2012, at 2:32 AM, Ryan Culpepper wrote:

> On 06/25/2012 11:53 PM, Stevie Strickland wrote:
>> On Jun 26, 2012, at 1:30 AM, Ryan Culpepper wrote:
>>> On 06/25/2012 10:25 PM, Stevie Strickland wrote:
>>>> As for the negative blame, who should be responsible?  Who's
>>>> responsible for the negative blame that gets triggered below, where
>>>> we have a generic interface for containers mapping names to public
>>>> keys?
>>>> (module GEN racket
>>>> ....
>>>> (define-generics key-container
>>>>   ([get-key (->   key-container? (->   string? prime?))]))
>>>> (provide gen:key-container))
>>>> (module IMPL racket
>>>> ....
>>>> (struct my-key-container (hash)
>>>>  #:methods gen:key-container
>>>>  [(define (get-key self) (lambda (str) (hash-ref my-key-container-hash str)))])
>>>> (provide (struct-out my-key-generator))
>>>> (module CLIENT racket
>>>> ....
>>>> (define gen (my-key-generator (hash '(("foo" . 5)))))
>>>> (define key-lookup (get-key gen))
>>>> (key-lookup 'bad-key))
>>>> I'd say the person who created the generator (that is, the person who
>>>> called the constructor, analogous to the object creator in the
>>>> interface story).  If they wanted to pass off key-lookup to someone
>>>> else without being blamed for bad  inputs, then they, of course,
>>>> could just provide it with a contract.
>>> Let's split the CLIENT module up into three regions/labels/submodules/whatever:
>>> C1: (define gen (my-key-generator (hash '(("foo" . 5)))))
>>> C2: (define key-lookup (get-key gen))
>>> C3: (key-lookup 'bad-key))
>>> C2 should be blamed. The contract on get-key is (or should be)
>>>  (->  key-container? (->  string? prime?))
>>> so C2 gets back a value that it is obligated to treat as a
>>>  (->  string? prime?)
>>> but it allows that value to be misused (at point C3, but it is C2 that incurred the obligation).
>>> It's definitely *not* the site of the creation of the generator (C1) that should be blamed.
>> For the most part, I agree that C2 looks like the most
>> straightforward source of negative blame.  I'd just stop the email
>> and say I agree, except for a problem that comes up in the following
>> case:
>>>> * What happens if you just call (get-a-prime 5) or (get-key 5)?  This
>>>> seems to be a whole different blame story than someone who just
>>>> creates a bad instance (like in your example), or who uses an
>>>> instance badly (like in my example).  In a way, that might be akin to
>>>> the following:
>>>> (define/contract (f x) (->   number? number?) x)
>>>> (f 3 5)
>>>> Instead of being a contract error, this is just a runtime error
>>>> (partially due to implementation details, but even if you erase the
>>>> contract, it's still a runtime error, so that's okay).  Similarly,
>>>> (get-a-prime 5) would be a runtime error without the contracts, so
>>>> I'm fine with it still being a runtime error with them, and only
>>>> having contracts mediate interactions that involve actual instances
>>>> of a generic structure.
>>> No, (get-a-prime 5) is just a contract error. The contract of
>>> get-a-prime is
>>>  (->  has-prime? prime?)
>>> The contract form for generic interfaces should protect both the
>>> interface itself (the capability to define structs that implement
>>> it) and the generic functions that it defines. (Just like the
>>> provide/contract struct form protects both the constructor and the
>>> accessors.)
>> Who is the positive blame in this case?  We don't have a particular
>> implementation to blame here.  The reason I chose C1, not C2, as the
>> negative blame is because the existence of a particular
>> implementation gives us the appropriate positive blame.  Without it,
>> I don't know how to contract this appropriately.
> The positive blame is GEN, because the client violated the contract of the *interface*, and that contract was applied by GEN.
> There are only two cases when an implementation is involved as a party: when the implementation violates the contract (see violent agreement above) or when the generic module violates the contract by misusing a method submitted by an implementation. For example, consider this elaboration of GEN:
> (module GEN racket
>  ....
>  (define-generics has-prime
>    (get-a-prime has-prime))
>  (define (get-two-primes hp)
>    (list (get-a-prime hp "first one")
>          (get-a-prime hp "second one")))
>  (provide-generics-with-contract
>    (has-prime [get-a-prime (-> has-prime? prime?)]))
>  (provide get-two-primes))
> The get-two-primes function calls get-a-prime with an additional string argument. The error should be GEN violated the contract on has-prime, with the appropriate IMPL module as the negative party. (I think.)

Wait, what?  First of all, GEN would be blamed here because it's using get-a-prime as the _client_, which means it would be the negative blame in any contract errors.  The error you've shown here (calling the function with too many arguments) is due to the negative party, not the positive one.  Second of all, if we call get-two-primes with a non-has-prime (e.g., (get-two-primes 5)), then we run into the same problem of not knowing WHICH implementation to blame for positive blame.

Here, let's examine the error I was discussing by looking at this variation on your original example:

(module GEN racket
 (define-generics has-prime
   (get-a-prime has-prime))
   (has-prime [get-a-prime (->   has-prime? prime?)])))

(module CLIENT racket
 (require 'GEN)
 (get-a-prime 5)) ;; ERROR

Why should GEN be blamed?  It neither implemented the generic interface (positive blame) nor used it (negative blame).  But who _should_ be the positive blame in the contract error above?  There's no implementation to blame at this point!

And if you do consider GEN the positive blame as a stand-in at this level, then what happens if we DO have an implementation?  That is, take the following variation on my example:

(module GEN racket
 (define-generics key-container
   ([get-key (->   key-container? (->   string? prime?))]))
 (provide gen:key-container))

(module IMPL racket
 (struct my-key-container (hash)
  #:methods gen:key-container
  [(define (get-key self) (lambda (str) 4))])
 (provide (struct-out my-key-generator))

(module CLIENT racket
 (define gen (my-key-generator (hash '(("foo" . 5)))))
 (define key-lookup (get-key gen))
 (key-lookup "foo") ; error due to IMPL
 (get-key 5))       ; error due to CLIENT

Here, we definitely want IMPL to be blamed for providing us a bad implementation in the first case.  But what do we do to make sure that we get the appropriate blame in both cases?  Do we need to perform two contract wrappings, first wrapping the generic function with the contract mediating between the CLIENT and GEN (to catch the second error above), and then, once we know which implementation we have inside the generic function, wrapping the specific implementation of that generic function with the same contract mediating between the CLIENT and IMPL (to catch the first)?

And in both our examples, we have only one value flowing in (the generic instance), but what if we took more arguments, where some of those arguments were higher-order (either state or behavior)?  If we do the double-wrapping described above, then GEN will take the positive blame for those arguments before IMPL does, but IMPL will be the only blame party actually using them, which means whatever it does here will blame GEN (or whatever positive party you choose for the initial generic function application).  That's just completely wrong.


Posted on the dev mailing list.