Protocol with constrained Self leading to weird compile error

Hi,

I have distilled an example of a problem I'm having compiling some generic code that is trying to create a situation where:

  1. a protocol can only be "conformed-to" by types that also conform to another protocol (using protocol A where Self: B)
  2. an extension on protocol A defines a helper function that uses a generic type that is generic over Self as mentioned in (1)

The code below is a "working" playground that fails to compile if the Mapped protocol has a Self constraint - with a strange compiler error message that the code DefaultBuilder<Self>() has a Self that does not conform to the expected types, when the extension it is in clearly has this constraint.

If you remove the Self constraint on Mapped the code compiles but of course Mapped can then be used on any type, and I think this has knock-on effects on the verbosity of the implementations of buildMappings which cannot rely on the compiler knowing that Self is already restricted.

import UIKit

protocol FeatureDefinition: class {
    var whatever: String { get }
}

protocol Builder {
    associatedtype FeatureType: FeatureDefinition
    
    func add(mapping: String, feature: FeatureType.Type)
}

class DefaultBuilder<FeatureType>: Builder where FeatureType: FeatureDefinition {
    func add(mapping: String, feature: FeatureType.Type) {
    }
}

/// REMOVE THE CONSTRAINT HERE and it will compile, but not do what is hoped
protocol Mapped where Self: FeatureDefinition {
    static func buildMappings<BuilderType>(intents: BuilderType)
        where BuilderType: Builder, BuilderType.FeatureType == Self
}

extension Mapped where Self: FeatureDefinition {
    static func collectMappings() {
        /// This is the problem when the protocol is constrained
        let builder = DefaultBuilder<Self>()
        Self.buildMappings(intents: builder)
        print("Yay!")
    }
}

final class TestFeature: FeatureDefinition, Mapped {
    var whatever: String { return "Testing" }

    static func buildMappings<BuilderType>(intents: BuilderType)
            where BuilderType: Builder, BuilderType.FeatureType == TestFeature {
        intents.add(mapping: "Some mapping", feature: TestFeature.self)
    }
}

TestFeature.collectMappings()

To further clarify my intent:

I want to make it so that types (call them F) conforming to this protocol can only be passed a builder of a type that works specifically with the type of F, so that when the builder is used, arguments passed to functions of the builder can also be constrained to F. So that the caller cannot just pass random things from other types to the builder.

Why the constraint? Doesn’t protocol inheritance work?

protocol Mapped : FeatureDefinition {
    ...
}

Your example compiles on master (so should compile in Swift 5). In Swift 4.2, there were a number of issues with placing constraints on Self in a protocol declaration's where clause, but @Slava_Pestov has since fixed them (most recently in Sema: Allow protocols with 'Self' constraints again by slavapestov · Pull Request #19844 · apple/swift · GitHub).

As @SDGGiesbrecht says, you can use the more consice form of:

protocol Mapped : FeatureDefinition {
  // ...
}

in order to workaround the issue in 4.2.

3 Likes

Why the constraint? Doesn’t protocol inheritance work?

Purely because the "is a" relationship isn't really clear there. But you're right it should work fine.

That's very interesting @hamishknight thanks for the detail.