Allow Optional variance when refining protocols

Swift has a lot of special case support for Optional.

One interesting use case that is not supported is strengthening the use of Optional in covariant positions in protocol requirements to be non-Optional.

For example, it would be nice if we could do the following:

protocol NonEmptyCollection: Collection {
    var first: Element
}

In this example, the compiler would synthesize a witness for the weaker first requirement from Collection that has type Element?.

It might also be nice to be able to relax non-Optional when in contravariant position in a requirement:

protocol Foo {
    func takesString(_ s: String)
}

protocol Bar: Foo {
    func takesString(_ s: String?)
}

Thoughts?

4 Likes

I'm not sure about allowing it on protocols, but we certainly should for conformers. The way I would do a non-empty collection would be to make it a concrete, generic wrapper rather than a protocol anyway:

struct NonEmptyCollection<C: Collection>: Collection {
  let base: C
  init?(_ base: C) {
    guard base.isEmpty == false else { return nil }
    self.base = base
  }

  // forward startIndex, endIndex, count, subscript, etc...
}

extension NonEmptyCollection {
  var first: C.Element { return self[startIndex] } // Should be allowed.
}

extension Collection {
  var nonEmpty: NonEmptyCollection<Self>? { return NonEmptyCollection(self) }
}

func doSomething<C: Collection>(_ input: C) {
  guard let data = input.nonEmpty else { return }
  // use 'data.first/last/min/max/etc' without optionals.
}

Currently this code fails to compile because .first must be an Optional<C.Element>, and the compiler does not implicitly promote the covariant witness.

Why would we allow it for conformers but not refining protocols? If conformers are allowed to provide an implementation with tighter constraints it may be very useful to speak about such conformers generically.

The question of how to best model NonEmptyCollection is tangential. This was just the most immediate example that came to mind. Wrappers like the one you show can be quite useful. But I can also imagine collection types which are always non empty. It would be a shame to have to use a wrapper with these types. They might be rare enough that it wouldn't make sense to include such a protocol in the standard library but it would be very useful for the language to allow us to express this kind of refinement when when the need arises.

3 Likes

The topic of this pitch is also the subject of SR-522, SR-731, SR-733, SR-1950, and SR-4161--and maybe others.

At least one WIP implementation exists: apple/swift#8718. I believe the consensus is that this has always been an intended feature and that it's just not been implemented yet.

1 Like

Thanks for the pointers. These links appear to talk about allowing covariance in conformances (as in Karl’s example). I don’t see any discussion about covariance and contravariance in refining protocols.

What is the practical difference between the requests to allow variance in conformers and your proposal?

Generic code that constrains a type to the refined protocol will not need to deal with Optional in the covariant case and would be able to provide Optional values in the contravariant case.

This would already be the case if the more constrained requirement is ranked higher than the other requirement (otherwise you would get an ambiguity error).

Oh, it looks like this is accepted already:

protocol Foo {
    var name: String? { get }
}
protocol Bar: Foo {
    var name: String { get }
}

It just isn’t useful yet because attempting to conform to Bar produces a compiler error that the type does not conform to Foo. It looks like maybe fixing the bug is all that is required. This is great!

That's odd, this works fine for me:

public protocol Wrapper: RawRepresentable {
    init(rawValue: RawValue)
}

public final class Box<RawValue>: Wrapper {
    public var rawValue: RawValue
    public init(rawValue: RawValue) {
        self.rawValue = rawValue
    }
}

Does the compiler treat variance differently for inits?

Yeah, I think a non-failing init can witness a failing init requirement.

1 Like

You don‘t even need the Wrapper protocol there for that. Failing init‘s can already be satisfied by non-failing inits just fine, so just conforming to RawRepresentable is enough.