Issue with `Protocol can only be used as a generic constraint because it has Self or associated type requirements`

Viewed 231

I'm trying to generate a ViewModel that conforms to a Protocol Protocoling, the protocol is generic, and has an associated type. There are a few ViewModel's that conform to the protocol, so I am trying to create a factory for the viewModel. I have encotuntered the following error by Swift: Protocol can only be used as a generic constraint because it has Self or associated type requirements Example code:

protocol Protocoling {
    associatedtype modulingType
    var data: modulingType { get }
}

enum MyTypes {
    case myName
    case myAddress
}

class NameViewModel: Protocoling {
    let data: String

    init(name: String) {
        data = name
    }
}

class AddressViewModel: Protocoling {
    let data: [String]
    init(address: [String]) {
        data = address
    }
}

class DataFactory {
    func viewModel(forType type: MyTypes) -> Protocoling {
        switch type {
            case .name: return NameViewModel(name: "Gil")
            case .address: return AddressViewModel(address: ["Israel", "Tel Aviv"])
        }
    }
}

The error is in func viewModel(forType type: MyTypes) -> Protocoling.

Is there a way to solve this issue?

2 Answers

You can use a protocol with an associated type (PAT) as a return type like that without more constraint because the compiler needs to know which type to use.

In your case you must use a technic called the type erasure to be able to work with any Protocoling:

class AnyProtocoling: Protocoling {
  let data: Any

  init<U: Protocoling>(_ viewModel: U) {
    self.data = viewModel.data as Any
  }
}

class DataFactory {
  func viewModel(forType type: MyTypes) -> AnyProtocoling {
    switch type {
    case .myName:
      return AnyProtocoling(NameViewModel(name: "Gil"))
    case .myAddress:
      return AnyProtocoling(AddressViewModel(address: ["Israel", "Tel Aviv"]))
    }
  }
}

This will allow you to "erase" the associated type of your protocol and return an Any version of your view model.

In order to understand why the PAT needs to work like that I like the following example: the Equatable protocol (which is a PAT):

static func ==(lhs: Self, rhs: Self) -> Bool

This function uses the Self type which is an associated type. You want to use it in the next generic function:

func areEquals(left: Equatable, right: Equatable) -> Bool {
  return left == right
}

Here the compiler will trigger this error: Protocol can only be used as a generic constraint because it has Self or associated type requirements. Why? Lets take this example:

struct tomato: Equatable {}
struct salad: Equatable {}

areEquals(left: tomato(), right: salad())

There is no reason to compare tomatoes and salads. The associated type Self is not the same. To avoid this error in this case you need to constraint the Self type as following:

func areEquals<T: Equatable>(left: T, right: T) -> Bool

Now you know the T are equatables and with the same associated types.

This is very simple to fix, in your concrete factory implementation you just need to specify a generic for your factory that has to conform to protocol protocoling, see code below :

Swift 4

protocol Protocoling {
    associatedtype modulingType
    var data: modulingType { get }
}

enum MyTypes {
    case myName
    case myAddress
}

class NameViewModel: Protocoling {
    let data: String

    init(name: String) {
        data = name
    }
}

class AddressViewModel: Protocoling {
    let data: [String]
    init(address: [String]) {
        data = address
    }
}

class DataFactory<T> where T: Protocoling {    
    func viewModel(forType type: MyTypes) -> T? {
        switch type {
        case .myName: return NameViewModel(name: "Gil") as? T
        case .myAddress: return AddressViewModel(address: ["Israel", "Tel Aviv"]) as? T
        default: return nil /* SUPPORT EXTENSION WITHOUT BREAKING */
        }
    }
}

It's a first step into the wonderful world of abstraction with protocols. You really create some amazing things with it. Though, I have to say, that personally it's not as intuitive as something like inheritance, it's a great little mind bending puzzle for creating decoupled and abstract systems, that are actually far more powerful.

Swift is a great introductory language, and I believe that it's protocol and extension mechanisms make it one of the more complex and interesting languages.

This design pattern is a great way setting up things like dependency injection.

Related