<p>In a structural type system, a interface is just a list of properties with types and anything that has those properties automatically implements the interface. Typescript is one such example but it is (purposefully) not sound. Sound structural type systems are possible though.</p>
<p>There has been some confusion about what it means to be covariant or contravaraint. Feel free to skip this part if you are already familiar with the terms.</p>
<p>Consider I have the following types:</p>
<pre><code>interface List&lt;T&gt; {}
interface HashMap&lt;T, U&gt;{}

interface Thing {}
interface Animal: Thing {}
interface Rabbit: Animal {}
</code></pre>
<p>If I have a function like this:</p>
<pre><code>function k(b: List&lt;Dog&gt;) {
    ...
}
</code></pre>
<p>I could only pass <code>List&lt;Dog&gt;</code> instances. Not <code>List&lt;Cat&gt;</code> or <code>List&lt;Thing&gt;</code>. However, if the function body looked like this:</p>
<pre><code>function k(b: List&lt;Dog&gt;) {
    print(b[0]) //actually a List&lt;Rabbit&gt; would have the same behavior
}

function k(b: List&lt;Rabbit&gt;) {
    b.push(new Rabbit); // Actually here a list of animals or a list of things would work too, since I can also push here
}
</code></pre>
<p>Thus I'm considering a syntax like this:</p>
<pre><code>function k(b: I&lt;in C&gt;) // covariant type, supports only reads, subtypes accepted
function k(b: I&lt;out C&gt;) // contravariant type, supports only writes, supertypes accepted
</code></pre>
<p>A function can accept both covaraint <code>in</code> and contravariant <code>out</code> types like this:</p>
<pre><code>function get(map: HashMap&lt;out Animal, in Animal&gt;) {
    Animal animal = map[Dog];
    print(animal)
}

function use&lt;T: Thing, B: Rabbit&gt;(p: HashMap&lt;T, B&gt;) {
    get(p);
}
</code></pre>
<p>Note there was some suggestion for auto-detection, but that wouldn't work in this case since it would transitively effect the <code>use</code> function, and any function that calls use etc. leading to errors often being very far away from where they are supposed to be.</p>
<p>If you wanted to write to the hash map, both would be contravariant:</p>
<pre><code>function put(map: HashMap&lt;out Animal, out Animal&gt;) {
    map[new Animal] = new Animal
} 

put(new Map&lt;Thing, Thing&gt;)
</code></pre>
<hr />
<p>This is theoretically sound. But I'm unsure how to formally specify this. What if you have something like <code>b: I&lt;in I&lt;out C&gt;&gt;</code>. What would that even mean? <code>I&lt;out I&lt;in C&gt;&gt;</code> is obviously nonsense, but how do I tell in a more complex example.</p>
<p>How do I tell if some combination of covariant and contravariant generics makes sense, if a specific type matches the type, and if a given type even makes sense?</p>

<types><structural-types>
How to support contravariant generics in a structural type system?
CC BY-SA 4.0
2023-05-31T20:42:05.537Z
I don't follow this question ─ whether or not a type parameter is covariant, contravariant or invariant is determined by the way that type parameter is used. The language can have `in` and `out` annotations for type parameters, but these wouldn't change the subtyping relation in a structural type system; the purpose of the annotations is just to indicate intent, and the compiler can check whether the annotations are correct. In your example of `I<T>`, the type is invariant in `T` because of how `T` is used in the type, unless the property is readonly in which case `I<T>` is covariant in `T`.
kaya3
CC BY-SA 4.0
2023-05-31T20:53:15.750Z
@kaya3 Auto detection is defiantly a option but it would be nice to know what parameters I can pass purely based on the function signature
mousetail 'he-him'
CC BY-SA 4.0
2023-05-31T20:59:16.407Z
"Auto detection" of what types are subtypes of what other types is, essentially, the defining feature of a structural type system. The parameters you can pass are those whose type is assignable to `I<C>`, and whether or not `I<D>` is assignable to `I<C>` depends on how `T` is used in the definition of `I`. A function can't just declare for `I` to be covariant or contravariant in `T` if it isn't already. If `I<D>` isn't a structural subtype of `I<C>`, then the function cannot simply decide that it is a subtype, any more than it can decide `string` is a subtype of `I<C>`.
kaya3
CC BY-SA 4.0
2023-05-31T21:17:30.783Z
@kaya3 The issue with autodetection is it means any function that calls this function and it itself generic might be autodetected to become invariant at any point, and then transitively any function that calls that function and eventually if there is a error with the variance it will be very far away from what actually caused the mistake
mousetail 'he-him'
CC BY-SA 4.0
2023-05-31T21:23:50.047Z
I don't follow. If `I<T>` is supposed to be covariant in `T` but you accidentally declare `I` in a way that it is invariant in `T`, and a function accepts `I<C>` and is called with an argument of type `I<D>`, the error will occur at that call-site, but there will not be cascading errors in other functions which call the function that contains the error. Anyway, the mistake would be in the declaration of the type `I<T>`, so if you do want to allow annotations that the compiler checks, the annotation belongs in the declaration of `I<T>`.
kaya3
CC BY-SA 4.0
2023-05-31T21:30:17.460Z
It sounds like perhaps what you want is for `I<T>` to use `T` in any way it likes, but for `I<in C>` and `I<out C>` to be two different types, each of which have only the members of `I<C>` where the parameter `T` occurs in contravariant or covariant position respectively. So e.g. if `type I<T> = {foo(): T, bar(x: T): void}` then `I<in C>` would be just `{bar(x: C): void}` and `I<out C>` would be just `{foo(): C}`. But then `I<out I<in C>>` wouldn't be nonsense, it would be the type `{foo(): {bar(x: C): void}}`.
kaya3
<p>In type systems I'm familiar with, you would specify covariance on the definition of a type, not on its use. For example:</p>
<pre><code>interface I&lt;in T&gt; {
    property: T
}
</code></pre>
<p>would mean that in <code>function k(b: I&lt;C&gt;)</code>, <code>C</code> is always covariant. If you want covariance, you would need to have a second interface definition. In this system, there is no equivalent to <code>I&lt;out I&lt;in C&gt;&gt;</code>.</p>
<hr />
<p><strong>Edit</strong></p>
<p>Let's work this out with a more concrete example. Let's say we have the following types: <code>Animal</code>, <code>Dog</code>, <code>Cat</code>, <code>List&lt;T&gt;</code>.</p>
<p>So we want to be able to give a <code>List&lt;Dog&gt;</code> to a function <code>pet(List&lt;Animal&gt;)</code>, but at the same time we want to prevent this situation:</p>
<pre><code>function new_friend(animals: List&lt;Animal&gt;) {
    animals.push(new Cat)
}

my_pets: List&lt;Dog&gt; = ...
new_friend(my_pets) // oops! there is now a Cat in a List&lt;Dog&gt;
</code></pre>
<p>Instead of having <code>function new_friend(animals: List&lt;out Animal&gt;)</code>, how about we decide that this is really about the type of <code>animals</code>?</p>
<p>We could have a syntax like:</p>
<pre><code>function pet(animals: in List&lt;Animal&gt;) {
    for animal in animals { // okay
        animal.pet() // okay
    }
}

function new_friend(animals: out List&lt;Animal&gt;) {
    animals.push(new Cat) // okay
}

my_pets: List&lt;Dog&gt; = ...
new_friend(my_pets) // !!! type error: List&lt;Dog&gt; is not a supertype of List&lt;Animal&gt;
</code></pre>
<p>This way you still don't have the <code>I&lt;in I&lt;out C&gt;&gt;</code> problem, but that might be more suitable to a structural typing. It may be worth looking into how TypeScript does this, I am not all too familiar with it so I can't comment.</p>
<p>If functions are first class values in your language beware that functions are generally covariant in their return type but contravariant in their argument types. That always trips me up.</p>

CC BY-SA 4.0
2023-05-31T19:30:19.157Z
2023-05-31T20:00:55.743Z
2023-05-31T20:00:55.743Z
Jasmijn
CC BY-SA 4.0
2023-05-31T19:31:11.623Z
That seems contradictory, that means it would in no context be possible to write to the property thus be impossible to even construct the type
mousetail 'he-him'
CC BY-SA 4.0
2023-05-31T19:39:52.743Z
Ah, I missed the "supports only reads" and "supports only writes" parts. I'm not sure how locking covariant to readonly and contravariant to writeonly would work in practice, but I can make another suggestion, I'll edit my answer.
Jasmijn
CC BY-SA 4.0
2023-05-31T20:24:38.493Z
Thanks, but what if I have a type like `HashMap<in Aminal, out Animal>` where I need both a in and out parameter on the same type?
mousetail 'he-him'
CC BY-SA 4.0
2023-05-31T20:39:49.263Z
I do not know, but I also do not know what it would even mean to have a `HashMap<in K, out V>`  in this context. I'm afraid this goes above my paygrade, I hope someone else can answer your question.
Jasmijn
CC BY-SA 4.0
2023-05-31T20:54:43.080Z
Basically it's a map that I can pass in any animal and would give me back any animal. If I can pass in a supertype of animal also it obivously doesn't matter and is still valid, and the same if it always returns some subtype of animal since that is still a animal.
mousetail 'he-him'
<p>The conventional way to do this in a structural system is to <em>use the structural system</em>: given a type</p>
<pre class="lang-none prettyprint-override"><code>type List&lt;T&gt; = {
    get(i : Integer) -&gt; T
    add(val : T) -&gt; None
}
</code></pre>
<p>then you can freely define consistent types</p>
<pre class="lang-none prettyprint-override"><code>type ReadList&lt;T&gt; = {
    get(i : Integer) -&gt; T
}
type WriteList&lt;T&gt; = {
    add(val : T) -&gt; None
}
</code></pre>
<p>and use those as needed. These types are compatible supertypes under ordinary structural variance rules, and so values can already flow into them when required:</p>
<pre class="lang-none prettyprint-override"><code>function j(b: ReadList&lt;Animal&gt;) {
    print (k[0])
}
function k(b : WriteList&lt;Rabbit&gt;) {
    k.add(new Rabbit)
}
j(new List&lt;Dog&gt;)
k(new List&lt;Animal&gt;)
</code></pre>
<p>A List of Dogs has a <code>get</code> method that always returns an Animal, so we're good there. A List of Animals has an <code>add</code> method that always accepts a Rabbit, so we're good there too. This is one of the strongest points in favour of structural types.</p>
<p><strong>We can produce these types mechanistically just by omitting all methods that are not consistent with the variance you want</strong> in the parameters. A standard List already has the remaining methods, so it's compatible with those types. A list of a super/sub type will also have compatible methods for writing/reading, just ones that impose weaker requirements.</p>
<p>The List will have other methods too, but you're not going to access those ones; if you really want to require that they be present (so that you can't <em>only</em> satisfy the read/write types above), you can replace the methods' parametric types with bottom/top types to make them present but useless, but I don't see huge value in this for a structural system. To me, being able to make ad-hoc implementations like this is the point.</p>
<hr />
<p>You can also do this for the HashMap examples, with only a little more complexity:</p>
<pre class="lang-none prettyprint-override"><code>type OutOutHashMap&lt;K,V&gt; = {
    set(key : K, val : V)
    has(key : K) -&gt; Boolean
    hasValue(val : V) -&gt; Boolean
}
function put(map: OutOutHashMap&lt;Animal, Animal&gt;) {
    map[new Animal] = new Animal
}
put(new HashMap&lt;Thing, Thing&gt;)
</code></pre>
<p>is fine, because that accepts two Animals, and</p>
<pre class="lang-none prettyprint-override"><code>type OutInHashMap&lt;K, V&gt; = {
    get(key : K) -&gt; V
}
function get(map: OutInHashMap&lt;Animal, Animal&gt;) {
    Animal animal = map[aDog];
    print(animal)
}
function use&lt;T: Thing, B: Rabbit&gt;(p: HashMap&lt;T, B&gt;) {
    get(p);
}
</code></pre>
<p>is fine too. Nothing needs to change for <code>use</code>: <code>HashMap&lt;Thing, Rabbit&gt;</code> already has a <code>get</code> method that accepts an Animal and returns an Animal, and so it's allowed to flow in.</p>
<p>You don't <em>need</em> any sort of annotations for these cases. You could <strong>use your <code>in</code>/<code>out</code> syntax to generate these types on an ad-hoc basis</strong> (just removing all methods that used the parameter type in the parameter/return type), or allow trait-algebra style type differencing if you liked, but it's ultimately just about saving the programmer effort and making sure you don't miss something out (especially when it changes).</p>
<p>It may be easier to expand out any other parameterised types used in method parameters or return position when generating the ad-hoc types to make this detection easier, but ultimately it's readily detectable in the same way you already determine recursive structural subtyping.</p>
<hr />
<p>For <code>b: I&lt;in I&lt;out C&gt;&gt;</code>, let's substitute in our <code>List</code> types for <code>I</code>: <code>b : ReadList&lt;WriteList&lt;C&gt;&gt;</code> - I think this is what you mean. This has type</p>
<pre class="lang-none prettyprint-override"><code>{
    get(i : Integer) -&gt; WriteList&lt;C&gt;
}
</code></pre>
<p>which is</p>
<pre class="lang-none prettyprint-override"><code>{
    get(i : Integer) -&gt; {
        add(val : C) -&gt; None
    }
}
</code></pre>
<p>which is a perfectly fine type - it's a list of lists we can add values of type C to, but not add new lists to and not read the values of inner lists.</p>
<p>For <code>b : I&lt;out I&lt;in C&gt;&gt;</code>, I don't think that it <em>is</em> obvious nonsense, although it may not (or may) be useful. We have <code>b : WriteList&lt;ReadList&lt;C&gt;&gt;</code>, or</p>
<pre class="lang-none prettyprint-override"><code>b : {
    add(val : ReadList&lt;C&gt;) -&gt; None
}
</code></pre>
<p>which is</p>
<pre class="lang-none prettyprint-override"><code>b : {
    add(val : { get(i : Integer) -&gt; C }) -&gt; None
}
</code></pre>
<p>This is also a meaningful type: we can add any <code>List&lt;C&gt;</code> or <code>List&lt;? extends C&gt;</code> to it, but not read any value from it.</p>
<p>You can <em>always</em> generate these types, just by removing the inconsistent methods. Some types may not have any suitable methods that don't use the parameter type both co- and contra-variantly at once: that's ok, you just get a universal type (i.e. with no methods); you can put anything into that type, but can't do anything with it once it's there. They may be useless, but they are what the programmer said, and they're not invalid.</p>
<p>If you are generating these ad-hoc, you could report an error on producing an empty type if you wanted to, or even if you end up with no methods that contain the parametric type (e.g. only <code>size</code> is left). It probably does indicate a mistaken assumption by the programmer, so this is reasonable, but it's not a soundness issue.</p>
<hr />
<p>To formally address your questions at the end: all combinations of generics make sense, but some lose any relation to the parametric type, and this may be something you want to make an error out of if you are generating them - but you can also just stay out of it and let the programmer make their own types at will. You can tell if a specific type matches the type by ordinary structural subtyping, just as you do for existing parametric types, and without needing any extra care for variance.</p>

CC BY-SA 4.0
2023-06-01T03:55:14.183Z
2023-06-01T03:55:14.183Z
Michael Homer
CC BY-SA 4.0
2023-06-01T17:22:11.963Z
This is a more thorough version of what I was trying to get at with my answer.
Bbrk24
<h2>Multiple interfaces</h2>
<p>Going off of Jasmijn's answer about the placement of <code>in</code> and <code>out</code>, it's not infeasible to just end up with a lot of interfaces for the various use cases. For a hypothetical but not farfetched example:</p>
<pre class="lang-cs prettyprint-override"><code>interface Iterator&lt;out T&gt;
{
  T? Next();
}

interface Collector&lt;in T&gt;
{
  void Add(T);
}

class Queue&lt;T&gt; : Iterator&lt;T&gt;, Collector&lt;T&gt;
{
  // ...
}
</code></pre>
<p>Note that the class itself is <em>in</em>variant in the generic type.</p>
<p>Your read-only hashmap example could be achieved with an interface <code>ReadOnlyMap&lt;in Key, out Value&gt;</code>.</p>

CC BY-SA 4.0
2023-05-31T21:23:25.207Z
2023-05-31T21:23:25.207Z
Bbrk24
# How to support contravariant generics in a structural type system?
Posted by: username_0 on 2023-05-31
In a structural type system, a interface is just a list of properties with types and anything that has those properties automatically implements the interface. Typescript is one such example but it is (purposefully) not sound. Sound structural type systems are possible though.
There has been some confusion about what it means to be covariant or contravaraint. Feel free to skip this part if you are already familiar with the terms.
Consider I have the following types:
interface List {}
interface HashMap{}

interface Thing {}
interface Animal: Thing {}
interface Rabbit: Animal {}

If I have a function like this:
function k(b: List) {
    ...
}

I could only pass List instances. Not List or List. However, if the function body looked like this:
function k(b: List) {
    print(b[0]) //actually a List would have the same behavior
}

function k(b: List) {
    b.push(new Rabbit); // Actually here a list of animals or a list of things would work too, since I can also push here
}

Thus I'm considering a syntax like this:
function k(b: I) // covariant type, supports only reads, subtypes accepted
function k(b: I) // contravariant type, supports only writes, supertypes accepted

A function can accept both covaraint in and contravariant out types like this:
function get(map: HashMap) {
    Animal animal = map[Dog];
    print(animal)
}

function use(p: HashMap) {
    get(p);
}

Note there was some suggestion for auto-detection, but that wouldn't work in this case since it would transitively effect the use function, and any function that calls use etc. leading to errors often being very far away from where they are supposed to be.
If you wanted to write to the hash map, both would be contravariant:
function put(map: HashMap) {
    map[new Animal] = new Animal
} 

put(new Map)


This is theoretically sound. But I'm unsure how to formally specify this. What if you have something like b: I>. What would that even mean? I> is obviously nonsense, but how do I tell in a more complex example.
How do I tell if some combination of covariant and contravariant generics makes sense, if a specific type matches the type, and if a given type even makes sense?
**Tags:** types structural-types
> **Comments:**
> **username_1**: I don't follow this question ─ whether or not a type parameter is covariant, contravariant or invariant is determined by the way that type parameter is used. The language can have `in` and `out` annotations for type parameters, but these wouldn't change the subtyping relation in a structural type system; the purpose of the annotations is just to indicate intent, and the compiler can check whether the annotations are correct. In your example of `I<T>`, the type is invariant in `T` because of how `T` is used in the type, unless the property is readonly in which case `I<T>` is covariant in `T`.  
> **username_0**: @kaya3 Auto detection is defiantly a option but it would be nice to know what parameters I can pass purely based on the function signature  
> **username_1**: "Auto detection" of what types are subtypes of what other types is, essentially, the defining feature of a structural type system. The parameters you can pass are those whose type is assignable to `I<C>`, and whether or not `I<D>` is assignable to `I<C>` depends on how `T` is used in the definition of `I`. A function can't just declare for `I` to be covariant or contravariant in `T` if it isn't already. If `I<D>` isn't a structural subtype of `I<C>`, then the function cannot simply decide that it is a subtype, any more than it can decide `string` is a subtype of `I<C>`.  
> **username_0**: @kaya3 The issue with autodetection is it means any function that calls this function and it itself generic might be autodetected to become invariant at any point, and then transitively any function that calls that function and eventually if there is a error with the variance it will be very far away from what actually caused the mistake  
> **username_1**: I don't follow. If `I<T>` is supposed to be covariant in `T` but you accidentally declare `I` in a way that it is invariant in `T`, and a function accepts `I<C>` and is called with an argument of type `I<D>`, the error will occur at that call-site, but there will not be cascading errors in other functions which call the function that contains the error. Anyway, the mistake would be in the declaration of the type `I<T>`, so if you do want to allow annotations that the compiler checks, the annotation belongs in the declaration of `I<T>`.  
> **username_1**: It sounds like perhaps what you want is for `I<T>` to use `T` in any way it likes, but for `I<in C>` and `I<out C>` to be two different types, each of which have only the members of `I<C>` where the parameter `T` occurs in contravariant or covariant position respectively. So e.g. if `type I<T> = {foo(): T, bar(x: T): void}` then `I<in C>` would be just `{bar(x: C): void}` and `I<out C>` would be just `{foo(): C}`. But then `I<out I<in C>>` wouldn't be nonsense, it would be the type `{foo(): {bar(x: C): void}}`.  
---
## 3 answers
### Answer by username_2 on 2023-05-31. Score: 6
In type systems I'm familiar with, you would specify covariance on the definition of a type, not on its use. For example:
interface I {
    property: T
}

would mean that in function k(b: I), C is always covariant. If you want covariance, you would need to have a second interface definition. In this system, there is no equivalent to I>.

Edit
Let's work this out with a more concrete example. Let's say we have the following types: Animal, Dog, Cat, List.
So we want to be able to give a List to a function pet(List), but at the same time we want to prevent this situation:
function new_friend(animals: List) {
    animals.push(new Cat)
}

my_pets: List = ...
new_friend(my_pets) // oops! there is now a Cat in a List

Instead of having function new_friend(animals: List), how about we decide that this is really about the type of animals?
We could have a syntax like:
function pet(animals: in List) {
    for animal in animals { // okay
        animal.pet() // okay
    }
}

function new_friend(animals: out List) {
    animals.push(new Cat) // okay
}

my_pets: List = ...
new_friend(my_pets) // !!! type error: List is not a supertype of List

This way you still don't have the I> problem, but that might be more suitable to a structural typing. It may be worth looking into how TypeScript does this, I am not all too familiar with it so I can't comment.
If functions are first class values in your language beware that functions are generally covariant in their return type but contravariant in their argument types. That always trips me up.
> **Comments:**
> **username_0** (2023-05-31): That seems contradictory, that means it would in no context be possible to write to the property thus be impossible to even construct the type  
> **username_2** (2023-05-31): Ah, I missed the "supports only reads" and "supports only writes" parts. I'm not sure how locking covariant to readonly and contravariant to writeonly would work in practice, but I can make another suggestion, I'll edit my answer.  
> **username_0** (2023-05-31): Thanks, but what if I have a type like `HashMap<in Aminal, out Animal>` where I need both a in and out parameter on the same type?  
> **username_2** (2023-05-31): I do not know, but I also do not know what it would even mean to have a `HashMap<in K, out V>`  in this context. I'm afraid this goes above my paygrade, I hope someone else can answer your question.  
> **username_0** (2023-05-31): Basically it's a map that I can pass in any animal and would give me back any animal. If I can pass in a supertype of animal also it obivously doesn't matter and is still valid, and the same if it always returns some subtype of animal since that is still a animal.  
---
### Answer by username_3 on 2023-06-01. Score: 5
The conventional way to do this in a structural system is to use the structural system: given a type
type List = {
    get(i : Integer) -> T
    add(val : T) -> None
}

then you can freely define consistent types
type ReadList = {
    get(i : Integer) -> T
}
type WriteList = {
    add(val : T) -> None
}

and use those as needed. These types are compatible supertypes under ordinary structural variance rules, and so values can already flow into them when required:
function j(b: ReadList) {
    print (k[0])
}
function k(b : WriteList) {
    k.add(new Rabbit)
}
j(new List)
k(new List)

A List of Dogs has a get method that always returns an Animal, so we're good there. A List of Animals has an add method that always accepts a Rabbit, so we're good there too. This is one of the strongest points in favour of structural types.
We can produce these types mechanistically just by omitting all methods that are not consistent with the variance you want in the parameters. A standard List already has the remaining methods, so it's compatible with those types. A list of a super/sub type will also have compatible methods for writing/reading, just ones that impose weaker requirements.
The List will have other methods too, but you're not going to access those ones; if you really want to require that they be present (so that you can't only satisfy the read/write types above), you can replace the methods' parametric types with bottom/top types to make them present but useless, but I don't see huge value in this for a structural system. To me, being able to make ad-hoc implementations like this is the point.

You can also do this for the HashMap examples, with only a little more complexity:
type OutOutHashMap = {
    set(key : K, val : V)
    has(key : K) -> Boolean
    hasValue(val : V) -> Boolean
}
function put(map: OutOutHashMap) {
    map[new Animal] = new Animal
}
put(new HashMap)

is fine, because that accepts two Animals, and
type OutInHashMap = {
    get(key : K) -> V
}
function get(map: OutInHashMap) {
    Animal animal = map[aDog];
    print(animal)
}
function use(p: HashMap) {
    get(p);
}

is fine too. Nothing needs to change for use: HashMap already has a get method that accepts an Animal and returns an Animal, and so it's allowed to flow in.
You don't need any sort of annotations for these cases. You could use your in/out syntax to generate these types on an ad-hoc basis (just removing all methods that used the parameter type in the parameter/return type), or allow trait-algebra style type differencing if you liked, but it's ultimately just about saving the programmer effort and making sure you don't miss something out (especially when it changes).
It may be easier to expand out any other parameterised types used in method parameters or return position when generating the ad-hoc types to make this detection easier, but ultimately it's readily detectable in the same way you already determine recursive structural subtyping.

For b: I>, let's substitute in our List types for I: b : ReadList> - I think this is what you mean. This has type
{
    get(i : Integer) -> WriteList
}

which is
{
    get(i : Integer) -> {
        add(val : C) -> None
    }
}

which is a perfectly fine type - it's a list of lists we can add values of type C to, but not add new lists to and not read the values of inner lists.
For b : I>, I don't think that it is obvious nonsense, although it may not (or may) be useful. We have b : WriteList>, or
b : {
    add(val : ReadList) -> None
}

which is
b : {
    add(val : { get(i : Integer) -> C }) -> None
}

This is also a meaningful type: we can add any List or List to it, but not read any value from it.
You can always generate these types, just by removing the inconsistent methods. Some types may not have any suitable methods that don't use the parameter type both co- and contra-variantly at once: that's ok, you just get a universal type (i.e. with no methods); you can put anything into that type, but can't do anything with it once it's there. They may be useless, but they are what the programmer said, and they're not invalid.
If you are generating these ad-hoc, you could report an error on producing an empty type if you wanted to, or even if you end up with no methods that contain the parametric type (e.g. only size is left). It probably does indicate a mistaken assumption by the programmer, so this is reasonable, but it's not a soundness issue.

To formally address your questions at the end: all combinations of generics make sense, but some lose any relation to the parametric type, and this may be something you want to make an error out of if you are generating them - but you can also just stay out of it and let the programmer make their own types at will. You can tell if a specific type matches the type by ordinary structural subtyping, just as you do for existing parametric types, and without needing any extra care for variance.
> **Comments:**
> **username_4** (2023-06-01): This is a more thorough version of what I was trying to get at with my answer.  
---
### Answer by username_4 on 2023-05-31. Score: 3
Multiple interfaces
Going off of Jasmijn's answer about the placement of in and out, it's not infeasible to just end up with a lot of interfaces for the various use cases. For a hypothetical but not farfetched example:
interface Iterator
{
  T? Next();
}

interface Collector
{
  void Add(T);
}

class Queue : Iterator, Collector
{
  // ...
}

Note that the class itself is invariant in the generic type.
Your read-only hashmap example could be achieved with an interface ReadOnlyMap.
---
