The point Sam is making is about the variance of Mutable-HashTable specifically. Its relationship to the HashTable supertype is a red herring (which is why changing the type to Mutable-HashTable didn’t help).
Immutable data structures, like lists and immutable hash tables, are covariant in their type parameters. That means, for example,
a <: b => (List a) <: (List b)
where “=>” means “implies” and “<:” means “is a subtype of.” The intuitive justification for this is that “a <: b” means “a value of type a can always be used in place of a value of type b.” For example, a list of integers can always be given to a function that expects a list of numbers.
This is not true of mutable data structures. Imagine you have bindings with the following types:
v : (Vectorof Integer)
f : (-> (Vectorof Number) Void)
Is (f v) well-typed? The answer is no. The reason is that f’s implementation could very well be the following:
(define (f x)
(vector-set! x 0 0.5))
If (f v) were allowed, it would insert 0.5 into v, but v is supposed to only contain integers! (This is the “two-way communication channel” Sam was alluding to.) Therefore, (Vectorof Integer) cannot be a subtype of (Vectorof Number). We say Vectorof is *invariant* in its type parameter, since
(Vectorof a) <: (Vectorof b) iff a = b.
The same issue applies to mutable hashtables. If your DB type is defined to be (Mutable-HashTable (U Symbol Number) (U DB DBValue)), and you try to pass a value of type (Mutable-HashTable Symbol Integer), the typechecker rightly rejects your program, as Mutable-HashTable is invariant in its type parameters, so the two types must be precisely identical.
A solution to your problem is therefore to ensure the types really do match precisely. That is what the type annotation you wrote does: it forces the type to be identical to DB, overriding the type that was inferred. One might hope that TR’s type inference could be better here, but it’s a trickier case than it looks.
An aside: some typed languages support “bounded polymorphism” to give the programmer more fine-grained control over variance. For example, in Java, a function that accepts a Map can promise not to mutate it by giving the function a bounded type:
void f(Map<? extends Foo, ? extends Bar> m)
The question marks serve as “wildcards” that indicate f may be called with any Map<K, V> where K <: Foo and V <: Bar. It is a compile-time error for f to attempt to add new elements to m, but it is perfectly fine for it to access elements. However, I don’t believe Typed Racket supports any form of bounded quantification or bounded polymorphism at the time of this writing, so functions in Typed Racket that operate on mutable data structures are forced to be invariant.
Alexis
> To view this discussion on the web visit
https://groups.google.com/d/msgid/racket-users/CAD7_NO4-cDE_3AHZ%3Dv9ieC%2BjYEvgXgWcQG_TGA0DqbmX%3DBH%3DYQ%40mail.gmail.com.