How to properly 'unify' protocols with similar members into a single protocol?

I'm not entirely sure how to describe what I'm asking without just giving an example. The issue I'm having is motivated by using the CoreImage framework from Apple, which vends different subclasses of CIFilters, but the return types of these vending methods are specified as
CIFIlter & SomeCIFilterProtocol

For example, the photoEffectFade() method has this signature:

open class func photoEffectFade() -> any CIFilter & CIPhotoEffect

(where CIFilter is a class and CIPhotoEffect is a protocol)

There are many protocols which have nearly identical definitions, but the important thing is that they all have the exact same declaration of var inputImage: CIImage? { get set }

public protocol CIPhotoEffect : CIFilterProtocol {
    var inputImage: CIImage? { get set }
    //...
}

public protocol CIThermal : CIFilterProtocol {
    var inputImage: CIImage? { get set }
    //...
}

public protocol CIComicEffect : CIFilterProtocol {
    var inputImage: CIImage? { get set }
    //...
}

In order to simplify the question, here is a similar example of what I am trying to accomplish:


/* 
 * Let's say AAA, BBB, and CCC are vended from some package.
 * They all have identical `var foo: Int { get }` members
 */
protocol AAA {
    var foo: Int { get }
    // ...
}

protocol BBB {
    var foo: Int { get }
    // ...
}

protocol CCC {
    var foo: Int { get }
    // ...
}
/*
 * Here, FooHaving is a protocol I want to implement 
 * which would 'unify' all of these similar above protocols
 */

protocol FooHaving {
    var foo: Int { get }
}
/*
 * Here I have some object that can deal with anything that has
 * a `foo: Int` member
 */
struct FooPrinter<T: FooHaving> {
    let fooProvider: T

    func printFoo() {
        print(fooProvider.foo)
    }
}
/*
 * Attempting to add protocol conformance to a protocol doesn't work
 */
extension AAA: FooHaving {} // error: Extension of protocol 'AAA' cannot have an inheritance clause
extension BBB: FooHaving {} // error: Extension of protocol 'BBB' cannot have an inheritance clause
extension CCC: FooHaving {} // error: Extension of protocol 'CCC' cannot have an inheritance clause

struct aaa: AAA {
    var foo: Int
}

/*
 * I would love to be able to do this, somehow,
 * but of course it fails because AAA does not conform to FooHaving
 */
let f = FooPrinter(fooProvider: aaa()) // error: Generic struct 'FooPrinter' requires that 'aaa' conform to 'FooHaving'
/*
 * My current 'workaround', which feels less than ideal,
 * is to have a type erased object that can take in any
 * of these separate (but similar) protocols.
 */

struct AnyFooProvider: FooHaving {
    private let _provider: () -> Int

    var foo: Int {
        _provider()
    }
    init(_ x: any FooHaving) {
        _provider = {
            return x.foo
        }
    }
    init(_ x: any AAA) {
        _provider = {
            return x.foo
        }
    }
    init(_ x: any BBB) {
        _provider = {
            return x.foo
        }
    }
    init(_ x: any CCC) {
        _provider = {
            return x.foo
        }
    }
}

Is there some way to elegantly tie these kinds of separate but similar protocols together? The type erasure workaround works, but it feels unwieldy and hacky.

It feels like this should be possible, but I am coming up short. Any thoughts are appreciated. Thanks!

1 Like

You won’t be able to have an instance with a protocol anyways — just concrete types, so why do you need to extend third-party protocols at all? I think just this declaration with your protocol is enough, then each concrete type just needs to implement your protocol along with third-party. The nice (at least from my point of view) addition is that you won’t be tied to third-party protocols, but actually can just conform any type to your protocol and use in the same way (e.g. useful property for unit-testing).

The objects vended from CoreImage don't have a concrete (nominal) type because the return type declared by the framework is CIFilter & CISomeEffect - that is the crux of this question. There is no concrete type to extend/conform to FooHaving in this case.

Here's what I'd try: embrace the fact that CIFilter is an Objective-C class and take advantage of the dynamism that grants you. You could introduce a Filter type that:

  • Implements a common interface for methods/properties that are shared across all CIFilter subclasses and dynamically dispatches calls to the appropriate methods
  • Implements one-off specific methods/properties for specific filters via an extension
  • Bonus points: use @dynamicMemberLookup so that your Filter type exposes the same properties as the original CIFilter
@dynamicMemberLookup
class Filter<RawCIFilter: CIFilter> {
    
    var raw: RawCIFilter
    
    init(_ raw: RawCIFilter) {
        self.raw = raw
    }
    
    func setImage(to image: CIImage) {
        raw.perform(Selector(("inputImage")), with: image)
    }
    
    subscript<T>(dynamicMember keyPath: ReferenceWritableKeyPath<RawCIFilter, T>) -> T {
        get { raw[keyPath: keyPath] }
        set { raw[keyPath: keyPath] = newValue }
    }
    
}

extension Filter where RawCIFilter == CIFilter & CIThermal {
    
    func somethingSpecialJustForThermal() { }
    
}

let filter = Filter(CIFilter.thermal())

print(filter.name) // dynamicMemberLookup
filter.setImage(to: .red) // common interface
filter.somethingSpecialJustForThermal() // special interface

I would use the same type erasure technique as OP, adding a little more reusable code at most, but the core idea would be the same.

Swift uses a nominal type system, although AAA, BBB and CCC are structurally similar, they are mutually unrelated. I believe It requires a manually written wrapper it we want to erase their distinctions in the interfaces.

I don't quite understand the issue here, as I'd just use this:

let filter = CIFilter.photoEffectFade()
filter.inputImage = myImage

If for some reason you want to work via a protocol:

protocol HasInputImage: CIFilter {
    var inputImage: CIImage? { get set }
}

extension HasInputImage {
    var inputImage: CIImage? {
        get {
            value(forKey: kCIInputImageKey) as? CIImage
        }
        set {
            setValue(newValue, forKey: kCIInputImageKey)
        }
    }
}

extension CIFilter: HasInputImage {}

let filter: HasInputImage = CIFilter.photoEffectFade()
filter.inputImage = myImage

Beware that not all CIFilters have inputImage (e.g. generators), attempting to get/set that key in those cases would give a runtime error.

1 Like

@jazzychad whole example by @tera and this line to highlight is what I meant as well, just was getting to write an example too long.

I didn’t meant that you need to work with some specific filter type, but rather with class CIFilter and extend it with protocol conformance, because if you want to have some generic FilterHandler<FilterProtocol>, to create instance of it you need instance of a type that conforms to FilterProtocol, which in that case will be CIFilter.

Protocols in Swift are not exactly types, they looks like one and in some ways can be used like types, but any FilterProtocol is a box for an instance of arbitrary type that conforms to the protocol, while itself this construct doesn’t conform to it.

1 Like

Precisely, so this solution is not viable.

I think perhaps I should not have given the specific CIFilter example b/c that is not really the focus of the question. It just happens to be the thing that triggered my question.

The question is whether tying together different protocols with similar members is possible through extension or some other kind of protocol syntax, but it seems like the answer is no.

As @CrystDragon rightfully pointed out, Swift has a nominal type system.

But what you're describing requires a structural type system (or at least some reflection escape hatches). You can achieve this in a language such as TypeScript.

Maybe this then?

protocol FooHaving {
    var foo: Int { get }
}

protocol AAA2: AAA, FooHaving {} // Option 1
typealias BBB2 = BBB & FooHaving // Option 2

struct aaa: AAA2 {
    var foo: Int = 0
}
struct bbb: BBB2 {
    var foo: Int = 0
}

let f = FooPrinter(fooProvider: aaa())
let b = FooPrinter(fooProvider: bbb())
1 Like

Protocol composition in that scenario is the way — A & B or protocol C: A, B {}. Plus some kind of wrapper type, which is, again, is composition in a classic OOP meaning.

In either way, you still need to consider how you are going to implement this requirement in the end. For example, on a generic type wrapper you can express constraints

struct FilterHandler<Filter> where Filter: CIFilter {
}

extension FilterHandler: FilterProtocol where Filter: CIPhotoFilter {
}

extension FilterHandler: FilterProtocol where Filter: CICosmicFilter {
}

I personally wouldn’t favor such way though. In practice this is usually complicates things in a long term.

1 Like