2022.09.27

2022.09 Vol. 1

A little bit of variance

Does Go need Variance?

First off, what is variance? It has to do with subtype within a programming language’s type system. Like, Cat is a subtype of Animal. It kinda makes sense that a Cat could be used where ever an Animal is requested, since it is an Animal. Variance deals with more complicated type situations, like the type an array holds or a function signature. Cat might be a subtype of Animal, but how are “function from Animal to String” and “function from Cat to String” related?

The most common use case is covariant method return type which allows a method to return a more specific type than what the signature declares. func() Animal can return a Cat. A less common use case is contravariant method parameter types which allows a method to accept a more general type. func(Animal) will work for all cases of func(Cat).

Are these ever used in Go? Nope. Go doesn’t support subtyping, so can’t support any variance. Go prefers to have any type conversions be explicit (e.g. i.(T)) instead of implicitly converting them (which is kinda required for the variance examples). Go does support assigning a Type which implements an Interface to an Interface though (semi-related, check out how much complexity generics added by dropping rules which mention type parameter). So technically maybe covariant return types could be supported? Nope. The Go FAQ has a blurb on why it doesn’t support the popular use case.

Go separates the notion of what a type does—its methods—from the type’s implementation. If two methods return different types, they are not doing the same thing. Programmers who want covariant result types are often trying to express a type hierarchy through interfaces. In Go it’s more natural to have a clean separation between interface and implementation.

That line got me thinking. Is covariance bleeding implementation details into interfaces?

An interface allows a consumer and producer to agree on how they will interact. If a consumer wants an Animal, but the producer only knows how to pass back a Cat, this is exposing implementation details. A consumer would have to understand that Cat implements Animal. The compiler could check this for the consumer, but the Go devs do not intend to add this functionality since its expensive for the compiler and doesn’t completely mitigate the cognitive load for the consumer.

So far, when I have thought I needed this, it usually meant I was trying to hide something which should probably just be exposed to the consumer.