Surprisingly permissive compilation

The compiler behaves a bit strange here, maybe I'm missing something?

Consider an inner generic type, with a constraint on the type:

struct MyInnerType<T: Error> {
    let t: T

I don't have to worry about this constraint here, presumably the compiler figures that you won't be able to construct a value with the wrong T anyway. This makes sense.

struct MyOuterType1 {
    func test<T>(_ value: MyInnerType<T>) { }

This is perhaps a bit less obvious, but it works in practice because you need to constrain it manually:

struct MyOuterType1b {
    func test<T>() -> MyInnerType<T> { fatalError() }

This also works because the entire outer type is constrained w.r.t T

struct MyOuterType2<T: Error> {
    func test() -> MyInnerType<T>  { fatalError() }

This rightly fails, because the outer type could have anything as T, and it is "in control" of T

struct MyOuterType3<T> {
    // T does not conform to Error
    func test() -> MyInnerType<T>  { fatalError() }

So why does this work? T is constrained, but not to Error.

struct MyOuterType4<T> {
    func test() -> MyInnerType<T> where T: Collection { fatalError() }

That function looks like it's available for any T that is a Collection, but in reality T will have to be both a Collection and an Error:

enum MyEnum { case one }
enum MyError: Error { case one }
extension Array: Error where Element: Error { }

func ttt() {
    // Collection and Error -> OK

    // Only Collection -> Referencing instance method 'test()' on 'Array' requires that 'Double' conform to 'Error'

    // Only Error -> Instance method 'test()' requires that 'MyError' conform to 'Collection'

    // Neither -> Compiler shows both of the errors

When the constraint is on the type though, it has to be the correct one:

struct MyOuterType4<T: Collection> { 
    // Type 'T' does not conform to protocol 'Error'
    func test(_ value: MyInnerType<T>) { }

This is not necessarily a bug, but I wonder if there's an explanation for the behaviour?

MyOuterType4.T can be anything conforming to Collection.

MyInnerType.T can be anything conforming to Error.

struct MyOuterType4<T> {
    func test() -> MyInnerType<T> where T: Collection { fatalError() }

You already constrained MyInnerType.T to Error, so you do not need to repeat that. By adding the conditional constraint to Collection, you are effectively ending up with T: Error & Collection.

As for the compiler errors: Swift tries to make errors as relevant as possible by providing the conformance requirements the type is closest to meeting. Since you provided a conditional conformance to Error for Array, but don’t meet the conditions, that’s what it shows. Array does conform to Collection, after all.

Does that make sense?

Maybe I was unclear, I am not surprised at MyOuterType4, it follows the logic you - and I - described, that the constraint inherent in MyInnerType will constrain the function. Further constraining it is orthogonal. But my point is that with that logic, MyOuterType3 should also be fine.

It seems that the mere act of adding a where clause, any where clause, makes the compiler remember that MyInnerType has a constraint, and applies it. Without the where clause, it doesn't.


Why do MyOuterType1, MyOuterType1b and MyOuterType4 work, but not MyOuterType3?

Inner -> Outer

I’m assuming you mean MyOuterType3.

Now that you mention it, I think that is a bug. It seems like the compiler makes Error conformance conditional if there are other conditions, even though those conditions do not include Error.

cc @Slava_Pestov

In a generic declaration, the "T : Error" requirement on a generic parameter "T" is inferred from the appearance of T in the "MyInnerType" type constructor, however this inference only runs if the declaration has generic parameters or a where clause. If it has neither, the inference step is not performed, and "T : Error" must be explicitly stated in an outer generic signature.

1 Like
Terms of Service

Privacy Policy

Cookie Policy