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

From: Ryan Culpepper (ryan at cs.utah.edu)
Date: Tue Jun 26 01:30:02 EDT 2012

On 06/25/2012 10:25 PM, Stevie Strickland wrote:
> [Hit Reply instead of Reply All, so fixing that here.]
>
> On Jun 25, 2012, at 11:53 PM, Ryan Culpepper wrote:
>
>> On 06/25/2012 09:27 PM, Stevie Strickland wrote:
>>> On Jun 25, 2012, at 11:21 PM, Ryan Culpepper wrote:
>>>
>>>> On 06/25/2012 09:04 PM, Asumu Takikawa wrote:
>>>>> On 2012-06-25 20:17:33 -0600, Ryan Culpepper wrote:
>>>>>> IIUC from your later message, you've implemented the generics
>>>>>> analogue of object/c (per-instance contract), whereas
>>>>>> prop:dict/contract is closer to class/c (per-type contract). It's a
>>>>>> little fuzzy because prop:dict/contract hacks in per-instance
>>>>>> contracts too in a kind of ad hoc way.
>>>>>
>>>>> That's a good point. The better analogy might be interface contracts vs.
>>>>> class/c. With generics, it is easy to control all points that an
>>>>> instance is created since constructors are just procedures. With
>>>>> classes, you can't get away with that since the instantiation forms are
>>>>> macros.
>>>>>
>>>>> The difference/advantage you might get with a per-type contract for
>>>>> generics is that you get a more interface-like blame story, as with
>>>>> interface contracts. Coverage isn't as much of an issue since you can
>>>>> just contract all constructors.
>>>>>
>>>>> Unfortunately, it's also not clear how to implement interface-like
>>>>> contracts for generics. Since the generics forms don't control the
>>>>> constructors, it's not obvious how to instantiate the blame at the
>>>>> construction site.
>>>>
>>>> You don't want to blame the construction site; the relevant
>>>> party is the implementation site, where the generic interface
>>>> is associated with concrete methods within a 'struct' form. See
>>>> the docs for 'struct-type-property/c' for an example.
>>>
>>> Well, there are two blame parties, right?
>>>
>>> Much like interface contracts mediate between the creator of a
>>> class (that implements the interface) and the client of that
>>> class (that instantiates objects from that interface), I would
>>> think the contracts for a generic interface would be between the
>>> creator of a specific instance (the implementation site) and the
>>> user of that specific instance (the constructor site).
>>>
>>> Stevie
>>
>> The analogy to interface contracts doesn't help me, because I don't
>> know anything about them. But I think I disagree.
>>
>> (module GEN racket
>>   ....
>>   (define-generics has-prime
>>     (get-a-prime has-prime))
>>   (provide-generics-with-contract
>>     (has-prime [get-a-prime (->  has-prime? prime?)])))
>>
>> (module IMPL racket
>>   ....
>>   (struct prime-box (val)
>>     #:methods gen:has-prime
>>     [(define (get-a-prime self) (prime-box-val self))])
>>   (provide (struct-out prime-box)))
>>
>> (module CLIENT racket
>>   ....
>>   (define p (prime-box 4))
>>   (get-a-prime p)) ;; ERROR
>>
>> I think IMPL should be blamed for violating the contract on gen:has-prime.
>>
>> As I see it, GEN establishes an obligation on implementors of
>> 'has-prime'. IMPL provides an implementation that turns out to be
>> faulty; it doesn't live up to the obligation imposed by GEN. CLIENT
>> is blameless; I don't see how the location of the constructor call
>> has anything to do with it.
>
> I agree that IMPL is the positive blame party here and should be
> blamed for not returning a prime?,

So far so good....

 > but that's because in this example
> it's not protecting itself from bad prime-box values.  It shouldn't
> suggest that it will, in fact, return prime numbers if it isn't
> protecting its constructor with a contract, or checking inside its
> implementation of get-a-prime that it is going to return a prime
> number without that constructor guarantee.

Why "but"? You have explained why IMPL is at fault. It accepted an 
obligation and failed to meet it.


> 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.

> So here's the issues I see with the blame story I'm considering:
>
> * No way to contract the constructor (Asumu pointed this out with his
> desired parametric checks), which means no easy protection against
> having bad instances created.  This is what causes the implementation
> to be blamed in your situation, whereas just contracting the
> constructor would have avoided it.  It'd be nice to have a form that
> allows for this to be part of the generic definition, instead of just
> forcing the user to have to remember to write:
>
> (provide/contract [prime-box (->  prime? has-prime?)])
>
> but that likely requires changes to the struct form that uses the
> generic definition.

The idea of synthesizing a contract for the constructor based on the 
contracts of its generic interfaces just doesn't make any sense.

What if the field of 'prime-box' were named 'prng-seed' instead of 
'val'? Would you feel differently about the contract that should 
"obviously" be applied to the constructor?

It's IMPL's responsibility to make sure the generic interface's 
contract, the behavior of the methods, and the contracts of the module's 
exports all fit together. If they don't, blame IMPL.

> * 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.)

In this sense, it is unlike struct-type-property/c, which only applies 
to the property itself, not to the property's associated accessor function.

If you use an uncontracted version of get-a-prime or get-key, OTOH, then 
you should get an arity error or an internal error from the attempt to 
get the generic method table or whatever.

Ryan

Posted on the dev mailing list.