Hi folks I'm trying to understand how Swift handles type comparison and typecasting of collection types. I'm attempting to filter a nested array based on its type:
let array: [Any] = [[5], ["five"], [String](), [5.55]].filter { $0 is [Double] }
// output: [[], [5.55]]
It seems like the empty [String] instance is included in the filtered output. It also works when I try to cast an empty array:
let stringArray = [String]()
if var doubleArray = stringArray as? [Double] {
doubleArray.append(5.55)
}
One way I could imagine this is happening is that there's a possible optimization mechanism that only allocates the array when it actually gets data assigned. And while in this "empty state", it can't really be compared because it's not really present.
Not sure my explanation attempt goes in the right direction but I'm curious about the reason this typecasting succeeds and how Swift compares collection types.
It would take me a bit to put together a full explanation and justification (and there are definitely trade-offs), but the easiest way to think about this is that casting to Array, Set, or Dictionary acts as an elementwise cast (like if you’d mapped over the collection), rather than preserving the original type. This is related to the compiler knowing that these three types treat their generic parameter as covariant (e.g. every array of UIButton is a valid array of UIView), something arbitrary types cannot access. It also explains why empty collections become “magic” transformable types: every element of the collection passes the “can you convert it” check.
The last type that behaves like this is Optional: UIButton? is a valid UIView?, and you can cast nil to any other Optional type.
I was aware the two examples are not same: the original one is runtime casting, mine is compile time initialization. I think Jordan's explanation about how compiler handles empty array (in my case it's an literal) helps to understand why an empty array literal works in my example but a non-empty array literal doesn't. It's because the compiler thinks an empty array is convertible to any array and this is true both at compile time and runtime.
Not so: in your example, the literal [] is not an empty array of some element type that's converted to an array of some other element type—not at compile time, and not even notionally. That's simply not how literals work in Swift.
correct. Array<T> always binds its ArrayLiteralElement to Self.Element, via associated type inference from Array.init(arrayLiteral:). this is chosen by the array, not the array literal.
there are situations where the array literal can choose what element type it uses, but this is not one of them.
I don't think it contradicts what I said. I said an empty array can be converted to an array with any Element type at compile time, not that it decides the Element type of the array.
Below is the code from stdlib:
public protocol ExpressibleByArrayLiteral {
/// The type of the elements of an array literal.
associatedtype ArrayLiteralElement
/// Creates an instance initialized with the given elements.
init(arrayLiteral elements: ArrayLiteralElement...)
}
extension Array: ExpressibleByArrayLiteral {
@inlinable
public init(arrayLiteral elements: Element...) {
self = elements
}
}
Note init(arrayLiternal:) takes a variadic parameter of type Element. That parameter is provided by the compiler and is made available to the function's body as an array of Element. The fact that [] works means an empty array literal can be converted to 1) a variadic parameter of anyElement type, and then to 2) an array of that type. I suppose step 1 occurs at compile time and step 2 occurs at runtime. And how to handle empty array literal in step 1 must be hardcoded in compiler somehow. That's why I thought it's worth mentioning it (though I didn't realize there is variadic parameter involved in my original post).
I wonder which part of my above understanding is incorrect? Where can I find more information about how literal works? Thanks.
Is that a good behaviour though? What bad would it make if that cast was outlawed (and resulted in a runtime error)? It is trivial to make an empty array of the target type without a need of having an empty array of another type.. Plus it's somewhat dangerous and inconsistent – say, I wrote that cast and tested my app, but it so happens that during those tests the array was empty. And then after the app is shipped users start triggering the unhappy path of non empty arrays, causing runtime traps.
I don't get it. There must be something happening between the time the compiler see a literal [] and the time the Array(arrayLiternal:) receives a variadic parameter. I described it as a conversion. You said it's wrong. I wonder how would you describe it? A literal has to be "converted" to another form to be used by other part of the code. If conversion is the wrong word, please suggest a correct one.
I imagine the element type comes from the declaration. In the example provided, T has to be known from another source, or the declaration won't compile. That is, the full type for the array variable has to be known from some other source than the literal.
Maybe I didn't express myself clearly. I didn't argue on this. What I'm trying to understand is why empty array literal works in my example but a non-empty array literal doesn't.
No. It's not necessary to know T. My code compiles.
Conversion means that there are two types and a value of one type is converted to a value of another type. An array literal does not have any type, and there isn't a value constructed (even notionally) other than the one of the declared type. There is no word I would use for describing "it" because there is no operation here to describe.
Ah, I think there's where the point of confusion might be for you. Ok, compiler doesn't consider [] in isolation, it has enough context to parse it to be of a proper type right away. Similar to how when I write var x: UInt8 = 42 and when compiler get's to 42 it treats it as UInt8 right away, instead of treating it as something else initially and doing some conversion as a second step.
In some other languages right-hand side expression is parsed independently without getting any information from the left hand side, but not in swift, where lefthand side `var x: UInt8 = ' affects how right hand side is "parsed" (I am using this term loosely).
ExpressibleByArrayLiteral was actually named ArrayLiteralConvertible in the beginning, but this was changed in Swift 3 precisely because there is no conversion taking place. You can read about that change here.
Thanks! That's the answer I was looking for. So the reason why [] works is because compiler translates it to [T]() while parsing the code:
struct Foo<T> {
// Compiler translates it to:
// var values: [T] = [T]()
var values: [T] = []
}
For non-empty literal, T should have constraint, otherwise the compiler can't successfully translate the code (because it wouldn't compile).
struct Foo<T: ExpressibleByIntegerLiteral> {
// Compiler translates it to:
// var values: [T] = [T](1, 2, 3)
var values: [T] = [1, 2, 3]
}
In other words, literals are processed and gone very early when compiling code. I think I understand it in general (e.g., in other languages like C), but I got confused in Swift due to those literal related procotols. The Takeaway: literal related protocols are not about how the compiler process literals, but about how the types on the left-hand side receive the values translated by the compiler from literals.
Thanks. It verifies what Xiaodi and Tera said.
I see your point now. Thanks for spending time explaining it.