Getting around (the lack of) covariance to write a generic collection

Hi all!

(If you're familiar with the problem of custom variance annotations in generics, you might want to skip this introduction.)

I've been developing a system in which I need to pass arrays by reference. To do that, I implemented a class called ArrayClass that wraps a normal Array. Here's a simplified version of it:

public final class ArrayClass<Element> {
	public var array: [Element]

	public init(array: [Element]) {
		self.array = array

I often run into the problem of having to cast ArrayClasses, for instance from ArrayClass<Int> to ArrayClass<Any>. To do that, I created an initializer:

public init<T>(arrayClass: ArrayClass<T>) {
	self.array = arrayClass.array as! [Element]

However, this initializer hits on the problem that is the lack of covariance in generics, and has to circumvent it with the force cast. For example:

let intArray = ArrayClass<Int>(array: [1, 2, 3])

// Compiles and works ok
let anyArray = ArrayClass<Any>(arrayClass: intArray)

// Compiles fine (unfortunately) and only crashes at runtime
let stringArray = ArrayClass<String>(arrayClass: intArray)

This was just to illustrate the problem. I've read many past discussions on adding custom variance to the language, and they seem to center on two things (with which I agree):

  • Variance would probably be a complex thing to add to the language and there hasn't been much empirical evidence that it would be worth it.
  • Most use cases seem to center on writing collections, and there might be an easier way to do that.

I would like to focus on what would be an easier way to add some type safety here. Some people on previous discussions have suggested making some of the compiler magic used on arrays public, but I don't really understand what that would entail.

My tentative suggestion: for this use case, it would be enough to allow something like this:

public init<T>(arrayClass: ArrayClass<T>) where T: Element {
	self.array = arrayClass.array as! [Element]

This where clause, in my head, would mean "only accept this function call if T is a subtype of Element". If that worked, everything else seems to fall into place. However, in the type-system, this where clause seems to mean something like "constrain T to the protocol or class Element", because I get this error:

// error: type 'T' constrained to non-protocol, non-class type 'Element'

So, yeah. Does this suggestion (of allowing such a where clause) make sense? Am I overlooking something really obvious and complicated? Does anyone have another suggestion?

This looks like another use case for generalized supertype constraints.

Oh yes, this looks a lot like what I wanted. This was before the “new” version of the genetics manifesto though, right? Do you think it’d still be equally applicable?

Yea it is still equally applicable. This enhances the expressivity of generic constraints. The improved UI for generics simplifies the syntax and expands the contexts in which constraints may be used.

I’m hopefully that somebody will implement this in the next year or two so we can bring a proposal forward. I don’t think it would be too controversial.

And the most recent thread that we really need to push forward:

1 Like

Thanks! I'll move the discussion to that thread.