Possibility of an explicit covariance attribute for generic types

Disclaimer: I'm no compiler expert and I have no idea if the idea is pure nonsense or not, but I'd like to discuss it here since for some reason I seem to recall that it was mentioned somewhere before, but I cannot find that discussion.

It would be interesting to know if it does makes sense and it's potentially a feature that we could push forward or not. Here I'm speaking of some kind of an attribute or keyword for generic types that would allow covariance explicitly. Furthermore it would interesting to know if such an attribute could be retroactively added to existing types such as KeyPath.

What do you guys think? If the idea of such a keyword/attribute was discussed before, I really would appreciate if we can link the previous threads to this discussion.

2 Likes

Just throwing this out there for how one language (Scala) does variance: https://docs.scala-lang.org/tour/variances.html. Variance is one of the more complicated things in the Scala type system, and it's not something that you use very often in my experience.

2 Likes

Kotlin, Scala, C# all do varience similarly, you mark the generic type as either in, out, or inout. I am not a fan of the explicit marking, since it clutters code. However, if the compiler could do it for you!

1 Like

Hello everyone!

This morning, I watched Paul Hudson's new video on phantom types, https://www.youtube.com/watch?v=9Vs4YDSpzHA . One usecase he brings up are typesafe generic state machines (see below), which could be wonderful if it was possible to store them. I one had the same idea and found the same problem with it, but finding out that others had thought about this as well sparked new interest on my side.

Eventually it crossed my mind that if one could specify if a generic type parameter is covariant or contravariant, this would do the job. Searching for proposals in that direction, I found this thread and wanted to at least mention the usecase I found for this.

Explanation:

A typesafe state machine can be thought of as

struct Machine<T>{}

a simple generic type that doesn't really use that type. One can then write a method like this:

extension Machine{
func transition<U>(with arrow: Transition<T,U>) -> Machine<U>{
Machine<U>()
} 
}

struct Transition<T,U>{}

You can then go ahead and create the arrows that fit one's business logic, and if you try to apply an arrow to the machine while it is in the wrong state, this is caught at compile time.

If we had a way to annotate a generic type as covariant, we could then store the current machine as a

Machine<Any>

This is of course just a compromise solution, because you would still have to downcast to the current state of the machine at runtime to apply transitions, but at least we have some place to store the variable. Composition of arrows is still a useful thing (at least, if the transitions actually "do something").

I'm not aware that one could make this work in today's Swift.

Not with this syntax, but it is always possible to store the value in a variable of type Any. If you want more control and/or type safety, you can define a type-erasing wrapper type that stores the Any value:

struct AnyMachine {
  private var base: Any

  init<T>(_ base: Machine<T>) {
    self.base = base
  }

  func unwrap<T>(type: T.Type) -> Machine<T>? {
    return base as? Machine<T>
  }
}

let m1 = Machine<Int>()
let t = Transition<Int, String>()
let m2 = m1.transition(with: t)
let wrapped = AnyMachine(m2)
wrapped.unwrap(type: String.self) // → .some(Machine<String>)
wrapped.unwrap(type: Int.self) // → nil

If your Machine type has API that you want to be able to call on the wrapped value, you'd have to define a protocol that both Machine and AnyMachine can conform to.

1 Like