Conform to protocols by fulfilling stricter requirements

Context

Protocols are great to express a set of required dependencies without specifying the underlying implementation. However composition becomes difficult when some requirements are themselves expressed in term of protocols.

Let's look at the following example:

// Here we define the requirements for videos to be displayed
// This could correspond to a UI element that is decoupled
// from any external dependencies as it specifies its own requirements
// through protocols
protocol VideoPosterDependencies {
  var posterUrl: URL { get }
}

protocol VideoPlaceholderDependencies {
  var title: String { get }
  var video: VideoPosterDependencies { get }
}

func handle(_ videos: [VideoPlaceholderDependencies]) {
  videos.forEach { item in
    display(item.title)
    load(item.video.posterUrl)
  }
}

Our API might return different kind of videos to display. For instance:

struct HLSVideoData: VideoPlaceholderDependencies {
  var title: String
  var video: Video

  struct Video: VideoPosterDependencies {
    var hlsUrl: URL
    var posterUrl: URL
  }
}

struct MP4VideoData: VideoPlaceholderDependencies {
  var title: String
  var video: Video

  struct Video: VideoPosterDependencies {
    var mp4Url: URL
    var posterUrl: URL
  }
}

Even though both types provide all the required properties, Swift doesn't allow to write this as VideoPlaceholderDependencies requires video to be of type VideoPosterDependencies and not of an implementing type.

The type conformance can be solved with an associate type on VideoPlaceholderDependencies, but this would then forbid a number of usages such as the display of a collection of various types [VideoPlaceholderDependencies], in addition to being a cumbersome burden for every call site.

Proposed change: Allow for types to conform to protocol when they implement equal or stricter requirements

Examples

protocol FooProtocol {}
extension String: FooProtocol {}
protocol SubFooProtocol: FooProtocol {}

Example 1: Getter returns a more specific value (1/2)

protocol BarProtocol {
  var foo: FooProtocol { get }
}
struct Bar: BarProtocol {
  var foo: String
}

Example 2: Getter returns a more specific value (2/2)

protocol BarProtocol {
  var foo: FooProtocol { get }
}
struct Bar: BarProtocol {
  var foo: SubFooProtocol
}

Example 3: Function returns a more specific value

protocol BarProtocol {
  func getFoo() -> FooProtocol
}
struct Bar: BarProtocol {
  func getFoo() -> String { "" }
}

Example 4: Function accepts a less specific value

protocol BarProtocol {
  func log(foo: String)
}
struct Bar: BarProtocol {
  func log(foo: FooProtocol) {}
}

etc.

Implementation
I am out of my depth here, but I want to share an observation.

It is possible to reach this desired state by handwritting boilerplate code that ideally would be hidden to the developer. Consequently I am hopping that a purely additive change to Swift is possible, regardless of whether the below implementation would be the best approach.

// Protocols
protocol VideoPosterDependencies {
  var posterUrl: URL { get }
}

protocol VideoPlaceholderDependencies {
  var title: String { get }
  // namespace the requirement to allow the implementing type to conform to several protocols on video
  var __VideoPlaceholderDependencies_video: VideoPosterDependencies { get }
}
// still make the VideoPosterDependencies accessible under `.video`
extension VideoPlaceholderDependencies {
  var video: VideoPosterDependencies { __VideoPlaceholderDependencies_video }
}

.

// Implementation
struct HLSVideoData: VideoPlaceholderDependencies {
  var title: String
  var video: Video

  struct Video: VideoPosterDependencies {
    var hlsUrl: URL
    var posterUrl: URL
  }

  // Fulfill the protocol requirement
  var __VideoPlaceholderDependencies_video: VideoPosterDependencies {
    // Well.. nothing to do!
    video
  }
}

.

// Usage
let item = HLSVideoData(...)
// resolved through HLSVideoData.Video
item.video.posterUrl
item.video.hlsUrl
// resolved through the VideoPlaceholderDependencies
(item as VideoPlaceholderDependencies).video.posterUrl
// (item as VideoPlaceholderDependencies).video.hlsUrl (💣 unavailable)

:point_right:This would conflict if HLSVideoData was to conform to two such protocols VideoPlaceholderDependencies and VideoPlaceholderDependencies2 and we were to call

(item as VideoPlaceholderDependencies & VideoPlaceholderDependencies).video

as the implementation to use is then undefined. This seems to be a reasonable situation to raise an error for.

3 Likes

Hmm, we could probably allow witnesses to be covariant. I mentioned it in Protocol Witness Matching Mini-Manifesto

1 Like

associated types can be used here

protocol VideoPlaceholderDependencies {
    associatedtype VideoConcrete:VideoPosterDependencies
    var title: String { get }
    var video: VideoConcrete { get }
}

No for instance you would not be able to have an array of [VideoPlaceholderDependencies] that could have different associated types

That's a great framing.