In a Swift protocol, can you have a func parameter that is a protocol where you can swap out with a different protocol definition if it also conforms to it?

Not sure exactly how to phrase this or if it's possible, but essentially what I'm wanting to do is this. I would like to have a protocol that includes a function that takes a protocol as a parameter, but swap out that protocol with a different one, which also conforms to said protocol.

e.g.

//Top level protocol
protocol ApplicationState: Equatable {}

//Feature specific protocols
protocol UserState: ApplicationState {
  var userDidChange: Bool { get set }
}

protocol FreezeState: ApplicationState {
  var freezeState: String { get set }
}

protocol Foo {
  func update(_ value: any ApplicationState)
}

//Implementation

struct Bar: Foo { 
  func update(_ value: any ApplicationState) { } <--------- Conformance as defined in the protocol.
  func update(_ value: any UserState) { } <--------- Ideally what I would like to do since UserState also conforms to ApplicationState.
}

I am able to achieve what I want by casting the type any ApplicationState as? any UserState but I would ultimately like to remove that casting step since I would need to that in all the locations where my Foo protocol is used.

Is this possible?

You would typically use protocol-based dispatch for this for this:

protocol ApplicationState {
  func beUpdatedBy(_ foo: Foo)
}

extension Foo {
  func update(_ value: ApplicationState) {
    value.beUpdatedBy(self)
  }
}

Not sure if that's exactly what I'm looking for. That doesn't seem to allow me to change to the protocol that I ideally want to use since it won't include the properties associated with UserState or FreezeState.

Here is a more detailed Bar implementation on what I wanting to do.

struct Bar: Foo {
  struct State: UserState {
    var userDidChange: Bool = false
  }
  var userState: State = State()
  
  mutating func update(_ value: any ApplicationState) {
    guard let value = value as? any UserState else { <---------- Other parts of the app would use a different protocol here, depending the needs.
      return
    }
    userState.userDidChange = value.userDidChange
  }
}

struct InternalAppState: UserState, FreezeState { <-------- Internal type that maintains state relevant to the App. It also conforms to the same protocols defined in different features.
  var userDidChange: Bool = false
  var freezeState: String = ""
}

//Again a very crude implementation of what I'd like to do.
let internalState = InternalState()
var bar = Bar()
bar.update(internalState) <------ Since "internalState" conforms to `ApplicationState`, I can pass it into the function, but it's opaque and the exact values I'm interested aren't readily available unless I cast as the `protocol` the receiving feature needs. I'm hoping to make this is reusable and reduce the amount of casting I need, if possible. 

For more context, the protocol's will be feature specific and unaware of each other, besides the InternalState which will have access the different protocols.

The issue here is that the protocol Foo is a contract that any conforming type must uphold, specifically saying that you must be able to accept any type conforming to ApplicationState. If you want to handle a specific type, you could use an associatedtype State: ApplicationState requirement, which allows each conforming type to choose the specific type it allows to be passed. (You won’t be able to use a type like any FreezeState here, though, because it doesn’t conform to ApplicationState)

2 Likes

Yep, I get that. I explored using an associatedtype in the protocol as well and what I'm attempting to do also didn't work.

protocol Foo {
  associatedtype: State: ApplicationState
  func update(_ value: State)
}

This gets me the API that I want, but the call site is where it breaks down. The type I'll be passing in is some ApplicationState and casting fails.

//Rough implementation
struct FeatureContainer<Feature: Foo> {
  let feature: Foo

  func receivedUpdate(_ value: some ApplicationState) {
    feature.update(value as? Foo.State) <------ This casting will fail since the concrete type passed in is not concrete Foo.State,
    but DOES conform to the protocol that constrained ` Foo.State` (protocol UserState).
    But at this point, I don't know which `ApplicationState` protocol the `Foo.State` is using. (FreezeState or UserState).
  }
}

I guessing what I want isn't possible to accomplish since the type could have multiple protocol conformances that also conform to ApplicationState.

Do constraints solve the problem?

extension Foo {
  mutating func update(_ value: some UserState) where State: UserState { }
  mutating func update(_ value: some FreezeState) where State: FreezeState { }
}
extension FeatureContainer {
  mutating func receivedUpdate(_ value: some UserState) where Feature.State: UserState {
    feature.update(value)
  }

  mutating func receivedUpdate(_ value: some FreezeState) where Feature.State: FreezeState {
    feature.update(value)
  }
}

@Quedlinbug Not a bad idea but I don't think that will work in my current app.

The core ApplicationState concrete type will be a single type that is composed of different protocols defined by the different features, i.e. the properties defined in the examples protocols FreezeState and UserState, to maintain a single source of truth. When a property on the ApplicationState concrete type changes (e.g. appState.userDidChange = true), it will post itself to the FeatureContainer, which will determines if any property defined in the properties it cares about have changed.

In classes the feature you are talking about is called "covariant return types" and "contravariant parameter types":

class SuperClass {}

class MainClass: SuperClass {}

class SubClass: MainClass {}

class Foo {
    func update(_ value: MainClass) -> MainClass {
        MainClass()
    }
}

class Bar: Foo {
    override func update(_ value: SuperClass) -> SubClass {
        SubClass()
    }
}

Swift protocols don't support this (could be said they require "invariant" return and parameter types. I think something like this could be considered for Swift, although it would make the language more complicated.