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)
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.