Dynamic protocol conformance

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.

I wonder why Content is not a protocol. Looks like there's some hidden requirement here.

Well, in this example, Content is a struct that's auto-generated from some protobuf data model object. In our app we don't want to expose this directly to the rest of the codebase so we made an abstraction layer of protocols that wrap these various unsightly things so that we can follow dependency inversion.

So that means you only ever interact with the protocol, not the concrete types? In general, a one to one relationship between protocols and concrete types is a bad idea, as you're duplicating interfaces for little benefit.

So that means you only ever interact with the protocol, not the concrete types? In general, a one to one relationship between protocols and concrete types is a bad idea, as you're duplicating interfaces for little benefit.

It's not a 1:1 relationship. We also have other types that conform to the same protocols for times when the data model is not one of the protobuf variety, or when we are using mocks.

If I understood your problem correctly, here is how you can solve it without a need for any new language features:

protocol Contentesque {
    var subtype: ContentesqueSubtype
}

enum ContentesqueSubtype {
    case image(Imageesque)
    case text(Textesque)
    case empty
}

protocol Imageesque: Contentesque { ... }
protocol Textesque: Contentesque { ... }

extension Content: Contentesqu {
    var subtype: ContentesqueSubtype {
        switch content {
        case let .image(image): return .image(image)
        case let .text(text): return .text(text)
        case let .empty: return .empty
    }
}

extension ImageContent: Imageesque {
    var subtype: ContentesqueSubtype { .image(self) }
    ...
}

extension TextContent: Textesque {
    var subtype: ContentesqueSubtype { .text(self) }
    ...
}

@Nickolas_Pohilets

Your example doesn't solve our problem... because it's based on the assumption that Contentesque has no other property than "subtype".

In my situation, "Contentesque" protocol declares several other properties, like "id" and "label"—which the underlying Content object possesses—but which are not found on TextContent or ImageContent etc.

So it's impossible to have protocol Imagesque: Contentesque, then extension ImageContent: Imagesque because ImageContent does not know what its parent Content object's values are for "id" and "label".

That is specifically why I'm making this proposal: so that we could effectively create a protocol wrapper with proper inheritance, around some data model objects that don't have any kind of real inheritance.

Another way to have the syntax might be:

extension (Content when self.content is TextContent): Textesque {
   ...
}

What about something like the below, where protocol composition adds the extra properties, and methods up in the shared protocol do the work?

The printMyself method is void and only does a print, but you could imagine it doing something more interesting. It might be hard to accept or return anything that had really varying types.

The Content protocol (none of the protocols below) does not have any associated types so you can use it as the type to a container without having to manage opaque types.

I was trying to get this to work with some generic functions with where clauses but it didn't seem to come together, so maybe I misunderstood something when I posted yesterday.

--Dan

import Foundation

protocol Content {
  var content_id: Int {get}
  func printMyself()
}

protocol ImageContent {
  var image: Data {get}
}

protocol TextContent {
  var text: String {get}
}

protocol EmptyContent {}

struct Image : Content & ImageContent {
  var content_id: Int
  var image: Data
  func printMyself() {
    print("Image \(content_id) \(image)")
  }
}

struct Text: Content & TextContent {
  var content_id : Int
  var text : String
  
  var uppercased : String {text.uppercased()}
  func printMyself() {
    print("Text \(content_id) \(text)")
  }
}

struct Empty : Content & EmptyContent {
  var content_id : Int
  func printMyself() {
    print("Ø \(content_id)")
  }
}

func doThings() {
  let c : [Content] = [
    Image(content_id: 3, image: Data()),
    Text(content_id: 2, text: "my text content"),
    Empty(content_id: 4)
    ]
    
  for i in c {
    i.printMyself()
  }
}

Output is:

Image 3 0 bytes
Text 2 my text content
Ø 4

We cannot add the content_id property from Content onto ImageContent because these structs are generated from .proto files (protobuf) by protoc

In the original post I may not have been clear about this, but because these structs are generated files, we cannot modify them in any way.

That's why it would be nice to have something like what I'm pitching. :smiley:

(If we could so something like what you're proposing then I would not be making this pitch, but unfortunately that's not the case.)

Right, I totally understand that.

I'm trying to get this clear in my mind myself so I'm working through all the examples I can find. Which is educational for me but not that helpful for you :smile:

The generated code aspect is a problem. The structs reflect the actual bytes on the wire, don't they? I guess it's not so much protoc itself as the data stream aspect. I should go look at protocol buffers again. Thanks for giving me the opportunity to think about this though.

I don’t think I understand the situation. You mentioned that Content has subtypes: ImageContent, etc, then that Content is a struct. It couldn’t be subtype within Swift type system since there’s no subtype relation with struct as supertype.

Is it correct to assume that ImageContent and Content are separated (auto-generated) struct? And that ImageContent and Content both conforms to Imageesque? Then what role does ContentType play in this scenario?

Yeah you're not understanding.

This is the important part from my original post:

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.

See: https://developers.google.com/protocol-buffers/docs/proto#oneof

Protobuf devs often use "OneOf" to create a kind of "fake inheritance" where, for example, you have a base type Content.proto, which then has a value "content" of type "OneOf_Content", where "OneOf_Content" is an enumeration with values like "TextContent" and "ImageContent" etc. Then TextContent will have just the extra properties that define a TextContent (e.g. some string property). Meanwhile the base type has the stuff that's shared by all types of Content, like "id" and "label" etc.

So the problem we're facing is to create a set of protocols to wrap this behavior in real inheritance rather than expose this nasty fake inheritance to the rest of our code.

The feature I'm pitching (dynamic protocol conformance) would allow us to do that, by allowing us to define which protocol among a predefined set of protocols a given instance is to be understood by the runtime as conforming to, by making that outcome conditional based on the value of some property.

Of course the underlying type has to actually conform to all the protocols, so we still have a compile-time guarantee that it conforms to at least one of them.

The problem is that in Swift 5 if you have a type that conforms to protocols A, B, and C, even if you cast it to C, it will still pass a check that asks whether it conforms to A or B. There's no way to specify that you want a given instance to only be considered as conforming to C (but not A or B).

Ok, so there is:

enum Content {
  case image(ImageContent)
  case text(TextContent)
  ...
}

and you want to do

protocol Textesque: Contentesque { ... }
protocol Imageesque: Contentesque { ... }

extension Content: Contentesque, Textesque, Imageesque { ... }
extension ImageContent: Imageesque { ... }
extension TextContent: Textesque { ... }

I still don't see why Textesque should refine Contentesque, or why Content should conform to Textesque and Imageesque. It doesn't look like they have anything to do with one another.

Terms of Service

Privacy Policy

Cookie Policy