Constraining `associatedtype` to something like AnyProtocol for compile time checking

I want to constrain two generic types together by their associatedtypes, so I can compile time check for paired protocol matching instead of doing a runtime check for something like this:

func bind<V: View, P: Presenter>(_ v: V.Type, _ p: P.Type) where V: P.View, P: V.Presenter { 
    // we know V & P are mutually configurable
}

// implemented with an @ unavailable to give a more helpful compiler error than not implementing it at all
@available(*, unavailable, message: "Mismatched V.Presenter <-> P.View. Double check the View type conforms to Presenter.View, and the Presenter type conforms to View.Presenter")
func bind<V: View, P: Presenter>(_ v: V.Type, _ p: P.Type) { 
    // we know V & P are _not_ mutually configurable
}

protocol View {
    associatedtype Presenter: AnyProtocol
}
protocol Presenter {
    associatedtype View: AnyProtocol
}

I'm already able to solve this with an ugly runtime check:

func bind<V: View, P: Presenter>(_ v: V.Type, _ p: P.Type) where V: P.View, P: V.Presenter { 
  guard P.self is V.Presenter.Type, V.self is P.UserInterface.Type else {
    assertionFailure(
      "Mismatched V.Presenter <-> P.View. Double check \(V.self) type conforms to \(P.self).View, and \(P.self) type conforms to \(V.self).Presenter"
    )
    return
  }
  // safe to proceed binding V & P together
}

But this feels like something meant for the Generics system

Right now this isn't possible because I can't guarantee the associated types won't be fulfilled with a struct and AnyProtocol doesn't actually exist :sweat:

Aside from bumping that I think AnyProtocol would be useful, is there a way to get the generic system to do something like this now?

1 Like

This should work. bind has only one generic parameter, and you cannot make mismatched views/presenters because there are where clauses that ensure you get Self after roundtripping through types

func bind<V: View>(_ v: V, _ p: V.P) {
    // we know V & P are mutually configurable
}

protocol View {
    associatedtype P: Presenter where P.V == Self
}
protocol Presenter {
    associatedtype V: View where V.P == Self
}

struct SampleView: View {
    typealias P = SamplePresenter
}

struct SamplePresenter: Presenter {
    typealias V = SampleView
}


//struct MismatchedView: View { // error: 'View' requires the types 'MismatchedView' and 'SamplePresenter.V' (aka 'SampleView') be equivalent
//    typealias P = SamplePresenter
//}

//struct MismatchedPresenter: Presenter { // error: 'Presenter' requires the types 'MismatchedPresenter' and 'SampleView.P' (aka 'SamplePresenter') be equivalent
//    typealias V = SampleView
//}
3 Likes

That doesn't compile unless the typealias'es go to concrete types unfortunately. Right now we can't bind an associatedtype to always be AnyProtocol, which leaves us open to fulfilling Presenter.View with a struct or something, and make a statement like V: Presenter.View invalid.

More concretely, I'm trying to make some compile time VIPER helpers, something to help a scenario like like:

protocol SampleViewInterface: View {}
struct SampleView: SampleViewInterface {
    typealias P = SamplePresenterInterface
}

protocol SamplePresenterInterface: Presenter {}
struct SamplePresenter: SamplePresenterInterface {
    typealias V = SampleViewInterface
}

struct SampleModuleBuilder { 
    func build() {
        let presenter = SamplePresenter()
        let view = SampleView()
        presenter.view = view
        view.presenter = presenter
        //...
    }
//... extension in Tests target or something
    func buildPresenterAndMocks() {
        let presenter = SamplePresenter()
        let view = MockView()
        presenter.view = view
        view.presenter = presenter
        //...
    }

Huh. No idea how to write that.

Yea, no. I'm pretty sure that's not supported in Swift. I think you can only do:

protocol View {
  var presenter: Presenter { get }
}

protocol Presenter {
  var view: View { get }
}

And you need to define a new set of protocols for each pair.

protocol View1 {
  var presenter: { get }
}
protocol Presenter1 {
  var presenter: { get }
}

Though they're already essentially the same one.