If/switch expressions choose [AnyHashable: Any] over [Int: Int]

Consider this code:

let dict = if true {
  [0: 3]
} else {
  [:]
}

What do you expect the type of dict to be? Personally, I assumed that it would be [Int: Int]. So I was surprised to see that the actual type of dict was [AnyHashable: Any].

This behavior seems counterintuitive. Both branches of the if statement would be valid [Int: Int] instances, and there is no reason why [AnyHashable: Any] would be necessary. Furthermore, the equivalent ternary conditional expression:

let dict = true ? [0: 3] : [:]

does result in an [Int: Int].

Is there a reason for this? Is this a bug? Should there be a warning here?

5 Likes

SE-0380:

This has two benefits: it dramatically simplifies the compiler's work in type checking the expression, and it makes it easier to reason about both individual branches and the overall expression.

It has the effect of requiring more type context in ambiguous cases.

It differs from the behavior of the ternary operator . . . .

However, the impact of bidirectional inference on the performance of the type checker would likely prohibit this feature from being implemented today, even if it were considered preferable. This is especially true in cases where there are many branches.

However, by analogy with the proposal discussion where nil absent context simply doesn't compile, it stands to reason that [:] in one branch should also fail to compile rather than causing the whole thing to be inferred as [AnyHashable: Any].

[Edit] Similarly, with array literals:

let foo = [] 
// error: empty collection literal requires an explicit type

let bar = if false { [] } else { [] }
// no error here: `bar` is of type `[Any]`

cc @Ben_Cohen

7 Likes

I wouldn't have expected it to try and infer the type here, for the very reasons you explained, but I'm very confused at the type it picked when it did try to infer it.

let dict: [_: _] = if .random() {
  [0: 3]
} else {
  [:]
}

Gives dict the expected type of [Int: Int], and I would have expected any automatic inference to give the same result as using placeholder types.

4 Likes

We've always had defaulting behavior for collection literals that is a bit more aggressive than what we allow for bare nil literals (e.g. allowing f([:]) but not f(nil) when the parameter provides no context), so I'm not sure the analogy is totally sound. However, I am still surprised that the if expression compiles since I would think we would in any case resolve [0: 3] to a different type than [:] which should be invalid...

Oh, wow, this... looks like a bug in how we're doing the inference for if expressions! I totally agree with you that this shouldn't provide any additional context, but it appears that introducing the placeholders actually provides a mechanism for 'linking' the branches. Placeholders use very similar machinery to implicit generic args and we can see a similar effect:

struct S<T> {
    var x: T
    init(x: T) {
        self.x = x
    }
}

func f(_ b: Bool) {
    let _: S = if b {
        S(x: Int())
    } else {
        S(x: .zero) // ok!
    }
}

The previous snippet compiles just fine, but if we remove the annotation from let _: S then it fails to resolve the .zero reference (because there's nothing providing the Int context other than the alternative branch).

1 Like