Search code examples
swiftgenericsdesign-patternsprotocolsassociated-types

Swift Protocol referencing itself and associated type


I have a classic implementation of Chain of Responsibility pattern with the following code:

protocol Request {

    var firstName: String? { get }
    var lastName: String? { get }

    var email: String? { get }
    var password: String? { get }
    var repeatedPassword: String? { get }
} 

protocol Handler {

    var next: Handler? { get }

    func handle(_ request: Request) -> LocalizedError?
}

class BaseHandler: Handler {

    var next: Handler?

    init(with handler: Handler? = nil) {
        self.next = handler
    }

    func handle(_ request: Request) -> LocalizedError? {
        return next?.handle(request)
    }
}

So I can create a PermissionHandler, LocationHandler, LoginHandler, a SignupHandler and combine them in chain. So far so good.

Now I want to create a Chain of Responsibility for other purposes, let's say a MediaContentPlayer CoR with different types of MediaContentHandlers and I thought to refactor and reuse the base code using generics.

So I started from the Handler protocol:

protocol Handler {

    associatedtype HandlerRequest
    var next: Handler? { get }

    func handle(_ request: HandlerRequest) -> LocalizedError?
}

but I get error "Protocol 'Handler' can only be used as a generic constraint because it has Self or associated type requirements".

Is there a way to reference the protocol inside the protocol itself when using associatedtype? Or another way to make the above code not dependent on a specific type?


Solution

  • You would look after something like this:

    protocol Handler {
        // ...
        var next: some Handler<HandlerRequest == Self.HandlerRequest>?  { get }
        // ...
    }
    

    The problem here is that Swift doesn't (yet) have support for opaque return types that are protocols with associated types.

    A solution to this limitation is to use a type eraser for the next property:

    protocol Handler {
        associatedtype HandlerRequest
    
        // shift the generic from a protocol with associated type to a generic struct
        var next: AnyHandler<HandlerRequest>? { get }
    
        func handle(_ request: HandlerRequest) -> LocalizedError?
    }
    
    struct AnyHandler<HandlerRequest> {
    
        private var _handle: (HandlerRequest) -> LocalizedError?
        private var _next: () -> AnyHandler<HandlerRequest>?
    
        init<H: Handler>(_ handler: H) where H.HandlerRequest == HandlerRequest {
            _next = { handler.next }
            _handle = handler.handle
        }
    }
    
    extension AnyHandler: Handler {
    
        var next: AnyHandler<HandlerRequest>? { return _next() }
    
        func handle(_ request: HandlerRequest) -> LocalizedError? {
            return _handle(request)
        }
    }
    

    This way you can benefit both the protocol, and having the next property tied to the handler request type that you need.

    As an added bonus for using protocols, you can still benefit the default implementation from the base class:

    extension Handler {
        func handle(_ request: HandlerRequest) -> LocalizedError? {
            return next?.handle(request)
        }
    }
    

    That's how cool protocols are in Swift, they allow you to avoid classes and use value types for as much as possible, by improving the concept of polymorphism.


    Usage example:

    struct LoginHandler: Handler {
        var next: AnyHandler<AccountRequest>?
    
        func handle(_ request: AccountRequest) -> LocalizedError? {
            // do the login validation
        }
    }
    
    struct SignupHandler: Handler {
        var next: AnyHandler<AccountRequest>?
    
        func handle(_ request: AccountRequest) -> LocalizedError? {
            // do the signup validation
        }
    }
    
    extension Handler {
        // Helper function to easily create a type erased AnyHandler instance    
        func erase() -> AnyHandler<HandlerRequest> {
            return AnyHandler(self)
        }
    }
    
    // now let's put the handers to work:
    let loginHandler = LoginHandler()
    let signupHandler = SignupHandler(next: loginHandler.erase())
    let someOtherAccountHandler = SomeOtherAccountHandler(next: signupHandler.erase())