Conforming to a Protocol with associated type for different associated types

It would be a very nice feature to be able for a class to conform to a protocol with associated type for different types.

Let's say I have a protocol with associated type that define a factory

protocol Factory {
    associatedtype Object
    func createObject() -> Object
}

Now I would like to have a factory that implement this protocol for multiple types such as UILabel, UIImageView, ect ...
The first problem would be that the current syntax does not allow it

class ActualFactory: Factory {
    typealias Object = UIView
    typealias Object = UILabel

    func createObject() -> UIView {
        return UIView()
    }

    func createObject() -> UILabel {
        return UILabel()
    }
}

Definitely feels wrong and should not be used as such, at least in my opinion.
A good solution would be to use a different syntax for protocol with associated type, a syntax similar to the one for generics.
We would create the protocol like this :

protocol Factory<Object> {
    func createObject() -> Object
}

And protocol conformance would be declared that way :

class ActualFactory: Factory<UIView>, Factory<UILabel> {
    func createObject() -> UIView {
        return UIView()
    }

    func createObject() -> UILabel {
        return UILabel()
    }
}

This would enable conformance to the protocol for multiple associated types.

As for the compiler part I am not informed at all on that part.
Would this be possible?

I have long wanted this feature so that Siesta can use distinct model types to dispatch resource observer events to separate handler methods:

class SomeViewController: ResourceObserver<Post>, ResourceObserver<[Comment]> {
    
    func resourceChanged(_ post: Resource<Post>, event: ResourceEvent) {
        // ...
    }

    func resourceChanged(_ comments: Resource<[Comment]>, event: ResourceEvent) {
        // ...
    }

}

However, my understanding is that this would require the oft-requested, oft-rejected feature of generic protocols, which the core team has explicitly ruled out for the foreseeable future.

My (imperfect) understanding is that the fact that a type can only adopt a single type for an associated type is the distinguishing feature of associated types. That is, with associated types, it’s possible to ask β€œWhat is the single, unique element type of this Collection?” whereas with generic protocols, one can only ask β€œWhat are the (possibly many) element types of this Collection?” I believe the stdlib makes heavy use of this unique conformance guarantee.

(Stdlib authors and experts who understand this better than I do, please correct me here if I’m off the mark.)

3 Likes

Your protocol isn't really correct - ActualFactory isn't really a factory, but a repository of factories. Each type should have its own conformer. The simplest way to do this today is with a generic type:

struct Factory<T> {
  private let _create: ()-> T
  func create() -> T { return _create() }
  init(_ creator: @escaping ()->T) { self._create = creator }
}

enum Factories {
  static let viewFactory  = Factory { return UIView() }
  static let labelFactory = Factory { return UILabel() }
  // ...
}

let aView = Factories.viewFactory.create()

So that's the simple case.

If you wanted to support multiple kinds of factories, you could, for example rename Factory<T> -> SimpleFactory<T>, add a ComplexFactory<T>, and create a common protocol for them:

protocol Factory {
   associatedtype Object
   func create() -> Object
}
struct SimpleFactory<T>: Factory { ... }
struct ComplexFactory<T>: Factory { ... }

enum Factories {
  static let viewFactory = SimpleFactory { return UIView() }
  static func cachedImageFactory(for path: String) -> ComplexFactory<UIImage> {
    return ComplexFactory(...)
  }
}

That's fine, and it works today.

The only "problem" here is that everybody can see which kind of Factory you return (SimpleFactory or ComplexFactory, respectively). Ideally you'd just like to tell them that you return Any<Factory where Object == UIImage> or something opaque like that, but that would require generalised existentials, which are on the roadmap sometime.

So for now you need to leak the information about which concrete type you use, or create an AnyFactory<T> wrapper to hide the implementation detail. But it's all possible.

2 Likes

AFAIK, we are discouraged from overloading methods based on returned value (i.e. the <T> parameter being only in the return value). With createObject, you'd be getting exactly that...

Generic protocols are the feature that would allow us implementing one protocol multiple times:

Excerp from the Generics Manifesto:

One of the most commonly requested features is the ability to parameterize protocols themselves. For example, a protocol that indicates that the Self type can be constructed from some specified type T:

protocol ConstructibleFromValue<T> {
  init(_ value: T)
}

Implicit in this feature is the ability for a given type to conform to the protocol in two different ways. A Real type might be constructible from both Float and Double, e.g.,

struct Real { ... }
extension Real : ConstructibleFrom<Float> {
  init(_ value: Float) { ... }
}
extension Real : ConstructibleFrom<Double> {
  init(_ value: Double) { ... }
}

For more:

It is literally the first thing under "Unlikely": "Features in this category have been requested at various times, but they don't fit well with Swift's generics system because they cause some part of the model to become overly complicated, have unacceptable implementation limitations, or overlap significantly with existing features."

I believe the way we model this is by exposing "views" of the underlying data. For example, String has multiple conformances to Collection, exposed as "views" (as Characters, unicode scalars, UTF8 code-units, etc). Each of them is a wrapper around the same underlying data.

The way to model your example in Swift would be to flip the protocol - instead of Real being constructible from a Float, it is Float which is expressible as a Real. Again, I would point to the standard library's LosslessStringConvertible as an example. This approach is also cleaner from an encapsulation perspective and makes it easier for other types to conform to the protocol; rather than writing an initialiser on Real, and build knowledge of Float or Double in to Real, the knowledge of those types stays within them, and they simply need to know how to express their own data as a Real.

It also allows you to write default implementations of var realValue: Real { get } in protocol extensions, which produce a Real by inspecting other properties. You couldn't write a default designated initialiser - it would have to be a convenience initialiser.

2 Likes