Proposal for generic protocols

+1. I don't like the "generic protocol" syntax for multiple-input protocols for exactly this reason.

I think an argument in support of that syntax would be similar to the argument in support of method syntax. In the case of methods it is very common to have function that is very closely associated with one of the parameters. Method syntax provides convenience by recognizing that and bundling the method with the type it is closely associated with.

The analogy with protocols would exist if it was common to have a protocol with multiple inputs where the semantics of the protocol is obviously most closely associated with one of those input types. I don't believe this is actually the case. As @Joe_Groff mentioned, the most widely known use cases do not exhibit this relationship, thus making "generic protocol" syntax a bad fit for the feature. Further, I think this syntax would be confusing and / or duplicative along side a generalized existential feature that used where syntax to constrain "input" types (currently just Self) and associated types.

All of that said, I would really like to see support for multiple input types added eventually. Unfortunately I don't have any good ideas for syntax to offer.

1 Like

If you're suggesting that Protocol<T> syntax should be used for the equivalent of Protocol<where AssocType=T>, then not only would I say that's confusing, but it wouldn't work anyway because you'd need to name them, so at a minimum you'd need Protocol<AssocType=T> (which means the plain Protocol<T> syntax is still open for use).

It's true that you could define the conversion in either direction. Rust actually defines it in both, with both a From<T> and an Into<T> trait, but it also automatically implements Into<T> in terms of From<T> so you only need to implement the former (except in edge cases, see the Into documentation for details, which don't apply to Swift as we don't have the restrictions that necessitate this). The documentation claims From "offers greater flexibility" than Into though it doesn't elaborate as to why.

That said, the fact that you can implement the conversion in either direction doesn't seem particularly relevant here, as either direction requires "input types" and the ability to conform to a protocol multiple times with different parameterizations.

I'm not sure what you mean by "most closely associated with one of those input types". We don't need multiple inputs in order to have input types be useful, even just having a single input is sufficient (in fact, all the parameterized trait from Rust that spring to mind immediately all have a single input type). The distinction here is in Rust you can conform to a parameterized trait multiple times with different parameterizations, but for any given parameterization there's only one conformance. So I can conform to Add<isize> and Add<String> but I can't have two different Add<isize> conformances with different output types.

This functionality is distinct from the desire to have existentials that support associated types.

The inputs to a "generic protocol" would include Self. This biases From towards the destination type (Self) and subordinates the source type (the type parameter). A different design might look closer to multi-parameter type classes in Haskell where all parameters are peers. How we could make something like that work syntactically in Swift isn't immediately clear to me but I think this direction should be explored before we commit to a design for multiple-input protocols.

1 Like

Thanks Joe for giving your time and great explanation!

Indeed I never thiught replacing associated type with protocol generics. I think only concrete types must be able to conform to generic protocols, whereas another protocol can extend existing protocol with associated type (e.g. Collection extends Sequence).
On the other hand, generic protocol allows us to create mutliple specialization like the example I’ve given in previous post.

The specific idea we had in mind was that:

protocol Collection<Element> { ... }

could act as sugar for:

protocol Collection {
  associatedtype Element
  ...
}

which would let you specify the most important associated types as positional generic arguments, while still letting you name arbitrary associated type constraints by some other means.

It's not really "conforming multiple times"; each conformance has a distinct set of input types, each of which only conforms once. Since this is the less common case, I think a less common syntax is warranted. As a strawman, in the vein of some of the proposed syntaxes for higher-kinded protocols, you could say that : (A, B, ...) declares a protocol as having multiple conforming types rather than one Self type. In such a protocol, each requirement would have to be qualified to the conforming type it applies to:

protocol Convertible: (A, B) {
  A.init(from b: B)
  B.init(from a: A)
}

and this would make it clear that the conformances are distinct:

extension (Data, [UInt8]): Convertible {}
extension (Data, UnsafeRawBufferPointer): Convertible {}

My recollection is Rust explored having tuples conform to traits prior to settling on the current parameterized design. This was specifically in the context of the Add trait for the + operator, so e.g. you'd say

impl Add for (isize, isize) {
    type Output = isize;
    fn add(self, other: isize) -> isize {
        // ...
    }
}

This was discarded in favor of Add<isize>, though I don't remember the precise reasons for this. One likely reason though is that having to declare that a tuple conforms to the trait is confusing (both in terms of implementing it, and in terms of looking up these implementations later).

1 Like

Neglecting for the moment OS upgrades, the compiler has full knowledge of all the types in Swift at compile time. It can do the optimizations by annotating each time a function is used; assume func makeIterator() -> Iterator<Element> then when make iterator was called the compiler could note what its specific type was for example, in let i = array.makeIterator() the compiler knows that i is IndexingIterator<Array<Element>>. This is what Java does at runtime, but because Swift is a closed system it can be done at compile time. (For an OS upgrade you would have to re-optimize :slight_smile:).

I just want to chip in to show how messy real code becomes due to protocol single conformance rule. Here is some simplified real iOS app code where a view controller calls two variants of ItemSelectorViewController class to display a picker for items of type Element. Things get very messy when the class acts as the selector's delegate to process user selections - see the workaround solution below where two artificial classes are created to deal with fact Swift won't allow two delegate conformances with different associated types. I don't propose a solution here, just want to point out real-word problems I've hit.

// Example view controller
class ExampleViewController: ViewController {
    
    // Show category picker
    func showCategoryPicker {
        let vc = ItemSelectorViewController(...)
        vc.setDelegate(delegate: ClassForISCategory(inner: self))
        present(vc, animated: true, completion: nil)
    }
    
    // Show time picker
    func showTimePicker {
        let vc = ItemSelectorViewController(...)
        vc.setDelegate(delegate: ClassForISTimePeriod(inner: self))
        present(vc, animated: true, completion: nil)
    }

}

// ItemSelectorViewControllerDelegate for category picker
class ClassForISCategory {
    var inner: ExampleViewController
    init(inner: ExampleViewController) {self.inner = inner}
}
extension ClassForISCategory: ItemSelectorViewControllerDelegate {
    typealias Element = Category
    
    // methods for ItemSelectorViewControllerDelegate
}

// ItemSelectorViewControllerDelegate for time period picker
class ClassForISTimePeriod {
    var inner: ExampleViewController
    init(inner: ExampleViewController) {self.inner = inner}
}
extension ClassForISTimePeriod: ItemSelectorViewControllerDelegate {
    typealias Element = TimePeriod
    
    // methods for ItemSelectorViewControllerDelegate
}

Note that, even if ExampleViewController could directly conform twice to the delegate protocol, you would still need to do something to pick which conformance you want at the vc.setDelegate call site, since it would no longer be inferrable which delegate implementation you want at that point.

Sorry I should have made clear that ItemSelector is a generic and the correct conformance is automatically selected as shown below.

protocol ItemSelectorViewControllerDelegate {
    associatedtype Element: ItemSelectorItem
    ....
}
class ItemSelectorViewController<Element>: UITableViewController where Element: ItemSelectorItem {
    func setDelegate<DelegateType: ItemSelectorViewControllerDelegate>(delegate: DelegateType) where DelegateType.Element == Element {
        ...
    }
}
Terms of Service

Privacy Policy

Cookie Policy