Can you restrict a protocol to a generic type regardless of the type constraint?

Viewed 158

TL:DR

How can I make this compile so the protocol can be applied to any concrete implementations of BaseObject, specifically so it can access someFuncUnrelatedToTypeConstraint (that's the real goal)?

@interface BaseObject <__covariant TypeConstraint> : NSObject

- (TestObject*)someFuncUnrelatedToTypeConstraint;

@end
protocol ProtocolOnlyApplicableToBaseObject : BaseObject {}

Note: This won't compile because it complains I need to specify the type parameter for BaseObject, but that's just it... I want this to apply to any/all BaseObject<T> types regardless of what T is.


Now the full code...

In our ObjC codebase, we have a concrete object and a generic defined like so (oversimplified for the example). This says only NSObject or one of its subclasses can satisfy the type requirement.

@interface TestObject : NSObject

@end


@interface BaseObject <__covariant TypeConstraint> : NSObject

- (TestObject*)someFuncUnrelatedToTypeConstraint;

@end

We also have these (again, oversimplified) ObjC subclasses, the last which omits the type, so it defaults to NSObject implicitly.

@interface ConcreteObjectA : BaseObject<UIView>

@end

@interface ConcreteObjectB : BaseObject<UIWindow>

@end

@interface ConcreteObjectC : BaseObject

@end

Now we're trying to write a Swift protocol and a paired extension that can only be applied to instances of BaseObject because the extension needs to call the function someFuncUnrelatedToTypeConstraint().

protocol ProtocolForBaseObject : BaseObject {
  associatedtype ReturnType:SomeTypeAcceptingTestObject
}

extension ProtocolForBaseObject {

    func someTest() -> ReturnType {
        let testObject = self.someFuncUnrelatedToTypeConstraint()
        return ReturnType(testObject)
    }
} 

The goal is so we can call this in Swift...

extension ConcreteObjectA : ProtocolForBaseObject {
  typealias ReturnType = ReturnTypeA
}
let objA = ConcreteObjectA()
let returnTypeA = objA.someTest()

extension ConcreteObjectB : ProtocolForBaseObject {
  typealias ReturnType = ReturnTypeB
}
let objB = ConcreteObjectB()
let returnTypeB = objB.someTest()

extension ConcreteObjectC : ProtocolForBaseObject {
  typealias ReturnType = ReturnTypeC
}
let objC = ConcreteObjectC()
let returnTypeC = objC.someTest()

The issue is ProtocolForBaseObject won't compile because it says I have to specify the generic type constraint...

Reference to generic type 'BaseObject' requires arguments in <...>

But the issue is I don't care what the type constraint for the generic is. I want this to apply to all instances of the generic as the function I'm trying to reach doesn't use that type anyway so there's no reason for the protocol to care what that type is.

To get around it, I tried adding NSObject as the type constraint, but this doesn't seem to work either since BaseObject<NSObject> != BaseObject<UIView> even though UIView is a subclass of NSObject.

protocol ProtocolForBaseObject : BaseObject<NSObject> {}

So how can you structure the protocol to apply to all concrete instances of the BaseObject generic, regardless of the type constraint?

4 Answers

I guess the compiler complains because BaseObject<__covariant TypeConstraint> requires a type but :

protocol ProtocolForBaseObject : BaseObject {
  associatedtype ReturnType:SomeTypeAcceptingTestObject
}

A swift protocol can't inherit an Objective-C class

Edit (I hate it too)

I would not call that inheritance, and I'm surprised it compiles:

@objc class Foo: NSObject {
    let foo: Int
    init(foo: Int) {
        self.foo = foo
    }
}
protocol Bar: Foo {
    
}

class Baz: Bar { // 'Bar' requires that 'Baz' inherit from 'Foo'
    
}

does not, the compiler complains about :

'Bar' requires that 'Baz' inherit from 'Foo'

It seems the compiler treats protocol ProtocolForBaseObject : BaseObject like protocol ProtocolForBaseObject where Self: BaseObject

Edit Again

class Baz: Foo, Bar {
    
}

Compiles which makes me say that class Baz inherits from Foo and conforms to Bar but maybe it's just nitpicking

You will need to create macro:

@interface _BaseObject <__covariant TypeConstraint> : NSObject
    
@end
@protocol _ProtocolOnlyApplicableToBaseObject

@end
#define BaseObject(Type) _BaseObject<Type><_ProtocolOnlyApplicableToBaseObject>

Usage:

@interface ConcreteObjectA : BaseObject(UIView *)

@end

@interface ConcreteObjectB : BaseObject(UIWindow *)

@end

@interface ConcreteObjectC : BaseObject(id)

@end

I would suggest using composition instead of inheritance:

@protocol ProtocolOnlyApplicableToBaseObject
@interface BaseObject <__covariant TypeConstraint> : NSObject

@property (nonatomic, weak) id<ProtocolOnlyApplicableToBaseObject> delegate

@end

Then your's subclasses will be able to set self as delegate or will be able to reuse some other implementation

Ok, so apparently I wasn't thinking 'Swifty' enough here. As outlined in my question, I have been trying to restrict the protocol ProtocolForBaseObject to only be applicable to BaseObject<T> instances regardless of what T is. The problem is Swift doesn't let you target a non-typed generic, as shown by the compiler message above.

But then I took a step back and asked myself 'What are you actually trying to solve here?' When I asked that question, it clarified the real reason is I needed access to someFuncUnrelatedToTypeConstraint from within an extension to the protocol.

Ok, so if you can't do that by restricting the protocol to an untyped generic, is there another way to solve that specific issue?

When I asked that, I'm almost embarrassed to see how obvious the solution was. It was right in front of me all along.

Forget about BaseObject<T>. Just have the protocol define that method as a requirement!

So now, instead of this (which again doesn't compile...)

protocol ProtocolForBaseObject : BaseObject { // <-- This line won't compile
  associatedtype ReturnType:SomeTypeAcceptingTestObject
}

extension ProtocolForBaseObject {

    func someTest() -> ReturnType {
        let testObject = self.someFuncUnrelatedToTypeConstraint()
        return ReturnType(testObject)
    }
}

I now have this, which works exactly like I had hoped, and definitely is more 'Swifty'.

protocol ProtocolForBaseObject {
  associatedtype ReturnType:SomeTypeAcceptingTestObject
  func someFuncUnrelatedToTypeConstraint()
}

extension ProtocolForBaseObject {

    func someTest() -> ReturnType {
        let testObject = self.someFuncUnrelatedToTypeConstraint()
        return ReturnType(testObject)
    }
}

Of course there is one slight downside to this approach. While this solves my particular need, that's because I only needed access to that one function. If however the extension needed access to the entire BaseClass object, that may require breaking BaseObject<T> into two and moving all non-generic-related methods into BaseObjectBase, then making BaseObject<T> inherit from that. Then I could restrict the protocol to BaseObjectBase since no type constraints are needed.

Related