Recently I had the pleasure to write a protocol abstraction layer for some Swift protobuf objects.
One weird thing about protobuf is that it's strongly typed but it lacks inheritance. Instead, as a sort of faux inheritance, types typically represent subtypes as values of an enumeration called OneOf_TypeName
.
For example you might have a type Content
, which has subtypes ImageContent
, TextContent
, and EmptyContent
. The Content
object will then have a property content
whose type is an enumeration OneOf_Content
with cases .text(TextContent)
, .image(ImageContent)
, and .empty(EmptyContent)
.
To create an abstraction layer over these protobuf-generated structs, I made something like this: Contentesque
, and then child protocols, Textesque: Contentesque
, Imagesque: Contentesque
, Emptyesque: Contentesque
. A given instance Contentesque
then specializes in one of these three children.
However since the generated protobuf TextContent
struct does not inherit from Content
struct, and does not duplicate any of its properties, it's impossible to extend TextContent
to conform to Contentesque
. As a result the only solution was to extend Content
struct to conform to Contentesque
and to each of the child protocols by providing conformance via an accessor returning the content: OneOf_Content
enum's value.
This is very kludgy, and it made me really wish for a better, more Swifty solution to this kind of issue (which I suspect is likely to affect anyone who uses protobuf for anything).
Proposed Solution
The solution I propose would be dynamic protocol conformance
. This would allow mapping each value of a special @dynamicProtocolConformance
enum to a non-PAT protocol. Here's how it might look in the above example to extend the Content struct to dynamically conform to one of the three child protocols based on its underlying enum value:
protocol Contentesque {
var content_id: Int
}
/// a protocol enum for use in dynamic conformance
protocol enum ContentType {
case image = Imageesque
case text = Textesque
case empty = Emptyesque
}
// DynamicProtocolConformance enum name stands in for "one of" its cases
extension Content: Contentesque, ContentType {
// extend Content to conform to ContentEsque
var id: Int { content_id }
// now provide a computed var that determines
// which ContentType this Content object will conform to
// at runtime
@dynamicProtocolConformance
private var subType: ContentType {
switch content {
case .image: return .image
case .text: return .text
case .empty: return .empty
}
// also we must below provide conformance to each of the protocols listed in ContentType
// as would normally be required in current Swift if Content conformed to each one
}
Now then later if we have some instance of Content, while the compiler knows that under the hood it conforms to ALL the protocols in ContentType enum, meanwhile, it will also know that it should be treated as only conforming to ONE of them (kind of like selective type erasure, I guess?).
That way later if you say guard content is Textesque
then it will only pass the guard if the enum's value was .text
.