Variance Elsewhere

The last post was mainly about the variance explanation and it was using Kotlin. But what about other languages? Let’s explore together.


TL;DR

LanguageGenericsdec. outdec. inuse outuse intype erasurebreak out rulesVariance available on
Kotlin✔️✔️✔️✔️✔️✔️✔️classes, interfaces
Java✔️✔️✔️✔️
Scala✔️✔️✔️✔️✔️classes, traits
C#✔️✔️✔️interfaces
Swift✔️✔️
Go✔️
Typescript✔️✔️
PHPStan✔️✔️

I will evaluate generics support by a few aspects:

  • declaration-site variance support (columns dec. out and dec. in);
  • use-site variance support (columns use out and use in);
  • type erasure support — using a generic type without a specific generic parameter;
  • breaking declaration-site covariant rule support — having a covariant interface that can use a generic argument in a contravariant position (as an argument);

Kotlin

Kotlin is my main language so I had to place it in the first position. If you take a look at the table, it supports the most advanced operations with generics: both declaration-site and use-site generics, type erasure via star projection, and surpass covariance (out rules) via an annotation. Read more in the previous article on Variance.

Java

You may see from the table that Java misses declaration-site variance — the interesting thing it is the only language supporting just use-site variance, it is much more common to support just declaration-site variance.

Java has a type erasure called wildcard type. The only difference to star projection is the handling of nulls. But that’s an attribute of Java in general.

public class CollectionUtils {  
    public static int count(Collection<?> collection) {
        return collection.getAll().size();
    }
}

Java is specific in another aspect — providing generic type parameters is itself optional. Not providing the type results in raw type and erasing the generic type information.

Because there is no declaration-site variance, there is no need (and way) for surpassing covariance rules.

Scala

Scala provides declaration-site variance with a powerful extension — Lower Type Bounds. This is quite an interesting way to allow covariant-type arguments in in position.

Declaration-site variance is not supported. However, Scala has quite rich type system and provides existential types. You may use them to gain type erasure:

def count(collection: Collection[_]): Int = 
 collection.getAll().size;

C#

C# falls into the “standard” equipped language category. Provides declaration-site variance, but misses type erasure or something similar. Functions like count has to be written as generic.

Also, covariant variance cannot be broken. The immutable list is not covariant in C# but implements IEnumerable interface which covariant is but doesn’t have any Contains() method.

Interestingly, C# supports variance only on interfaces.

C# is another language that was originally released without generics. But in contrast to Java, you can pass generic type instead of the non-generic.

Read a little about the comparison C# to Java in an interview with Anders Hejlsberg, author of C#.

Swift

Swift language doesn’t support variance in generics.

To use generic without specifying the generic argument type, you have to create your own type erasure. This type erasure wrapper will then provide access to data without a generic argument. Type erasures have to be created manually. Swift also supports Opaque types and they could provide needed behavior for a “generic interface”; opaque types are supported only as return types. More on opaque types in the next article.

struct AnyCollection {
    private let getHelper: () -> AnyObject[]

    init<Entity>(stack: Collection<Entity>) {
        var mutableStack = stack
        self.getHelper = {
            return mutableStack.get() as AnyObject[]
        }
    }

    func get() -> AnyObject[] {
        getHelper()
    }
}

The example shows type erasure which will help us to count collection without knowing the generic type:

func count(collection: AnyCollection): Int = {
    return collection.getAll().length
}

Go

Go languages is quite late to Generics. The generics were added just a few days ago and are still in development. The design proposal explicitly mentions that variance is not solved for now.

I cannot simply find anything on type erasure in Go.

Typescript

Typescript supports generics for a long time, but variance is not implemented. But it is not missing so much because Typescript uses Structural subtyping. Thanks to structural subtyping A is an instance of B even when they are not sub/super types to each other at all (in the normal way of thinking). So typescript’s invariance is minimized to to “has the same interface?” question and therefore its generics allow subtypes.

interface Entity {
    name: String;
}
class Car implements Entity {
    name = "Car"
}
class Audi extends Car {
    name = "Audi"
    version = "A5"
}

class Collection<T extends Entity> {
}

let audis = new Collection<Audi>()

// this is ok,though generic is not covariant
let cars: Collection<Car> = audis

Typescript also does not check if the used generic parameter type is accepted by the generic argument’s bound. So the count function may be written like:

function count<T>(collection: Collection<Entity>): number {
    return collection.getAll().length;
}

PHPStan

PHPStan is a static analyzer for PHP and adds generics via PhpDoc annotations. That ensures type safety in a similar way as compilation — the program is checked before deploying. PHPStan is also quite new to Generics.

Currently, PHPStan supports only generic declaration-site covariance (via an annotation) and internally it has some prepared infrastructure for contravariance. But it cannot be defined by an annotation for now, only via a PHPStan’s extension.

PHPStan misses use-site variance. Star projection (~ type parameter omission) is also unsupported (actually, it is treated as an error, but it may be surpassed). The same applies to breaking covariant rules.

Variance is supported on classes and interfaces. It is being discussed if it will be supported elsewhere.

Conclusion

Compiling this post was extremely hard. But such exploration always opens new worlds and thinking! But it also shows that many languages use quite different type systems and it is not so easy to compare them.

I’d like to thank Petr Zvoníček for helping me to understand Swift and Jan Tvrdík for his note about Scala’s feature.

Please, comment if there is any mistake or omission.

Revisions:

2020/07/29 — added explicit info in which contexts is variance supported;