Some generics type inference questions

Let’s start with a simple one:

class F<T> {}

class B<A, F> : F<A> {}

Here, the compiler can’t infer F as a class, apparently because of the possible subsequent where clause, which requires to infer F as a the generic parameter.

A similar example with protocols:

class A<T, U> {}

protocol P {
    associatedtype A
}

protocol P1: P {

    associatedtype B
    
    func foo()  -> A<B, B>
}

The compiler treats A only as an associated type not taking into account existing types.

A more interesting example:

class Foo<T, U> {}

protocol P {
    associatedtype A
}

protocol P1 {

    associatedtype B

    func foo() -> Foo<A, B>
}

class Foo1: P1 {

    func foo() -> Foo<Bool, Int> {
        
        return Foo<Bool, Int>()
    }
}

Above the associatedtype A is not inferred as Bool in Foo1
An error will be present until I add typealias A = Bool

Once associatedtype A is required directly in P1, it is inferred just fine:

protocol P1 {

    associatedtype A
    associatedtype B

    func foo() -> Foo<A, B>
}

class Foo1: P1 {

    func foo() -> Foo<Bool, Int> {
        
        return Foo<Bool, Int>()
    }
}

It would be nice to know whether these inferences are omitted for performance reasons, other reasons or just worked out to be this way

These are just examples of shadowing. In your example:

class F<T> {} // This declaration of `F` is shadowed by...

class B<A, F /* this generic argument */ >: F /* is the generic argument, not class F */ <A> { }

See the inline comments.

1 Like

I guess this is kind of a hybrid case… Shadowing happens when you can’t tell what type/variable you are referring to, so there are certain conventions, but here it is clear what F am I referring to, so it’s a matter of inferring it. Not the same way expression types are inferred of course, but you get what I mean. It should be treated as class F

This would be rightful shadowing though:

class A<T, U> {}

func foo() {

   class A<R, S> {}
}

or

class F {}
class B<A, F> : F {} 

since you can’t tell what F is in the inheritance expression

I think I disagree with your assessment that the class F should be inferred in the first example (and similarly for the shadowing in the second example). It would be confusing and cause a lot of bugs if the shadowed type (class F) was inferred as a fallback here if the generic parameter (F) wasn’t suitable. You say it’s clear which one you are referring to, but I don’t think that’s the case for anyone reading the code, or indeed for most people writing it, especially when the types aren’t right next to each other in the source code.

The third one seems to show a real limitation of type inference, possibly for performance reasons. As this isn’t a shadowed type situation, it seems entirely unrelated to the first two examples. I’m not sure why you’re attaching screenshots instead of using the code feature but here’s an example if anyone wants to play around with it:

class Foo<A, B> {}

protocol P {associatedtype A}

protocol P1: P {
    associatedtype B
    func foo() -> Foo<A, B>
}

class Bar: P1 {
//    typealias A = Bool;
    func foo() -> Foo<Bool, Int> {
        return Foo<Bool, Int>()
    }
}
2 Likes

The names are simple and impractical for the example. Expressions too can be complex and unclear in terms of type for a person, but in Swift they are clear to the compiler thanks to type inference in most cases (Unable to infer complex closure return type). Even if it isn't clear to me as a side watcher, although it should be because I am explicitly referring to F<A> by providing parameters, it can be more that clear to the compiler, and in that case type safety is respected and there is no reason for a bug as a result of this.

Although F seems to be shadowing F<A>, I am not sure this can formally be considered type shadowing. Type shadowing should take place when there are uncertainties. But there aren't any. The compiler simply isn't smart enough to resolve the types.

True. Turned everything to md

It is a limitation for performance reasons. It can get really complicated and slow down compilation if it goes up multiple levels. See this thread:

Looks like it was rejected previously, but eventually limited to a single conformance

Yes, the current limited associated type inference is a compromise to keep some of the convenience, while avoiding the worst of the performance issues.

For more information and a possible way to improve things, I recommend reading this post from Matt. Gallagher. Specifically this part.

Dramatically improving the performance of type inference might be an interesting subject for a Ph.D thesis…

Okay, I guess it can be considered shadowing or not shadowing depending on your perspective about what exactly constitutes the “name” here, e.g. whether it includes the generic parameter. Either way, however, the value of doing this inference (e.g. that you don’t have to locally rename a generic parameter) seems to be far outweighed by the potential confusion this would cause when interpreting code, and the confusing error messages that would result if someone was intending to refer to a generic parameter but it was accidentally resolving to some other type.

All the existing forms of shadowing cause a lot of issues (e.g. people accidentally shadowing a generic parameter on a type with a generic parameter on a method on that type, “T != T”) but these are tolerated because of the benefits to cross-module extensions, retroactive conformance, etc. The advantages in this case seem very weak in comparison because, at least as far as I can tell, this only causes issues that are easily fixed locally.

Sounds interesting, will take my time to look through it. Lets see if we can take down that exponent to at least a decent power function in the near future.

I suppose, if it really is that big of a problem.

These error messages you mention shouldn't actually arise. What you are talking about would happen in case of ambiguity, which is resolved by shadowing.

However, these issues belong to people. I mean, its a mistake, not a compiler error. This is somehow similar to saying " quantifier expressions in higher math are confusing and cause a lot of issues. Not only they are hard to understand when long, but, for instance, people accidentally rearrange quantifiers which can lead to expressions meaning completely different things." Well, that is true, and to people who are unfamiliar with the mathematical apparatus this indeed happens, as well as what you mentioned - to people relatively unfamiliar with Swift. However, that obviously doesn't mean we have to give up on whatever causes accidents or confusion.

I must agree, though partially. The issue in question isn't that important right now. It is important in the same way some not-very-frequently-used method of Array. Not vital, can be implemented without any special effort, yet a part of the vast functionality Array vends and the Standard Library itself. Type inference is also a considerable part of Swift's semantics and a feature that in some sense makes Swift stand out. I believe this is something the compiler eventually has to be capable of as type inference enhances and the language evolves. Right now it is either severely unperformant to infer types in such cases or the issue simply hasn't reached the hands of the core team.

Here are some more examples:

class A<T> {}

class Foo {

    func foo<A>(_ arg: A) {

        let a = A<Bool>()
    }
}

It is quite obvious I am referring to an existing type A<T>. Although I am explicitly disambiguating A, the compiler keeps shadowing in favor of the parameter A. Here is an analogous case:

class Foo {

    let n = false

    func foo<A>(_ arg: A) {

        let n = false

        print(n, self.n)
    }
}

self.n is shadowed by n until I explicitly disambiguate with self. This is of course much simpler to solve though.

This one is more contradictory. Like my very first example. However, there is still no ambiguity.

class A<T> {}

class Foo<U> {

    func foo<A>(_ arg1: A, _ arg2: A<U>) {}
}

I want to note that there are moments when you can't infer a type in the general case.

class A<T> {}

class Foo {

    func foo() {
        
        class A<T> {}
    }
}

I’m not sure what you mean by “shouldn’t” here. Of course it would be better if users never wrote incorrect code but unfortunately they do. For example, going back to your very first example, someone might think that the F<A> on class B<A F> constrains the F in the generic parameter to be a generic type that can take A as a parameter (probably gets more likely when there are additional constraints on A and F like protocol conformance).

Yes, exactly, and one of the many tradeoffs considered when designing the language is avoiding features that are likely to be used incorrectly or confuse people. These downsides are weighed against the benefits e.g. the current generic shadowing issues, which are quite common and confusing, are tolerated because alternatives would break code evolution, retroactive conformance, etc. I don’t think the cases you’re illustrating here have those benefits, as they seem easily fixed by entirely local renaming of a generic parameter, not requiring editing dependencies or changes throughout your codebase. Is there a case where this isn’t true? Are you encountering this issue so often that this local renaming is a burden? I completely understand and accept that this disambiguation is possible, I’m just finding it hard to understand the upsides of implementing it.