For-in vs forEach() when sequence element is Dictionary.Key

Consider the following extension:

extension Dictionary {
    func f1(keys: any Sequence<Key>) {
        for key in keys {
            let value = self[key]
        }
    }
    func f2(keys: any Sequence<Key>) {
        keys.forEach { key in
            let value = self[key]
        }
    }
}

In for-in the type of key is inferred as Any, while in forEach() its type is Key. Is this by design or a bug?

First off, you probably know this, but both f1 and f2 should really take arguments of type some Sequence<Key>, not any, which would make the error go away.


As I understand it, this is a known limitation that could be lifted in the future. Let me try to break it down.

Starting with f2: forEach can preserve the concrete Key type thanks to Covariant Erasure with Constrained Existentials in SE-0353: Constrained Existential Types. So this is relatively straightforward.


In the case of f1, the compiler essentially rewrites the for loop into a while loop, like this:

var iterator = keys.makeIterator()
while let key = iterator.next() {
    …
}

If we write this while loop by hand, we get an error on the makeIterator() call in the first line:

error: inferred result type 'any IteratorProtocol' requires explicit
coercion due to loss of generic requirements

The compiler forces us to write keys.makeIterator() as any IteratorProtocol to acknowledge that the iterator variable loses the knowledge that its Element == Key, and this is the reason why the element type in Any in your for loop.

Pretty much this exact situation is described in "Losing" constraints when type-erasing resulting values in SE-0352: Implicitly Opened Existentials:

When the result of a call involving an opened existential is type-erased, it is possible that some information about the returned type cannot be expressed in an existential type, so the "upper bound" described above will lose information.

That section also mentions that future compiler versions could increase the expressivity of the language by recovering more type information that right now. It even makes explicit mention of primary associated types SE-0353 that could help with this.

In summary, it seems like the compiler could (and should, eventually) preserve the element type information when making an iterator from an any Sequence<T>, but that hasn't been implemented yet (and if implemented, it presumably would also have to go through Swift Evolution).

6 Likes

Thank you for the explanation. Complex stuff, though still interesting.

1 Like