So, you have a purse with silver coins in it, also with the corresponding label on the purse "silver coins". Then you change the label to just "coins". Then you add a nickel coin into the purse - and that doesn't invalidate the claim of the label. Then you put the old "silver coins" label back on that purse – at that point you are lying and it should come at no surprise that the coin you are getting out of that purse could be not silver but something else.
Note that transition from "silver coins" to "coins" here was a "safe claim", while the transition from "coins" to "silver coins" was an "unsafe claim", hence the "as!" ping and quite possible runtime error ("crash").
var x1 = [1, 2]
var y1 = x1 as [Any]
print(y1 as! [Int])
y1[1] = "string"
print(y1 as! [Int]) // ❌ runtime crash
var x2 = (1, 2)
var y2 = x2 as (Any, Any)
print(y2 as! (Int, Int))
y2.1 = "string"
print(y2 as! (Int, Int)) // ❌ runtime crash
struct PairOfAny { var first, second: Any }
struct PairOfInt { var first, second: Int }
var x3 = PairOfInt(first: 1, second: 2)
var y3 = x3 as PairOfAny // 🛑 Cannot convert value of type 'PairOfInt' to type 'PairOfAny' in coercion
// var y3 = x3 as! PairOfAny // ❌ runtime crash
print(y3 as! PairOfInt) // would be a runtime crash if compiled
y3.second = "string"
print(y3 as! PairOfInt) // would be a runtime crash if compiled
we can see that:
[Any] is a supertype of [Int]
(Any, Any) is a supertype of (Int, Int)
PairOfAny is NOT a supertype of PairOfInt
From a naïve "user" point of view there should be no significant difference between:
struct PairOfAny { var first: Any; second: Any }
struct PairOfInt { var first: Int; second: Int }
The latter is just a shorthand version of the former, expressions of which can be written adhoc without having to declare an explicit type.
On my book, if we consider (1) and (2) to be right, then (3) is wrong and needs fixing. And vice versa if we consider that (3) is right (as @ExFalsoQuodlibet suggests) then (1) and (2) is wrong and needs fixing.
I'm not really sure this is so different from @tera's example as you're making out. In the line var ys = xs as [Any], you are making a new 'instance' of the array, insofar as assignment of a value type like this forms a semantic copy of the original. It shouldn't be surprising that mutating operations can change the underlying dynamic type, and if your objection is that tera's example uses a full reassignment, we can achieve the same result without exposing the assignment to the user:
protocol P {
init()
}
class B: P {
required init() {}
}
class C: B {}
extension P {
mutating func change() {
self = .init()
}
}
var c = C()
var b: B = c
b.change()
b as! C // fail!
This seems quite natural reasoning, but I'm not sure this is a rigorous way to think about types. In other words, the fact that you can change the label to "coins" doesn't necessarily mean that you should be allowed to do that.
Consider the Coin type. I can do something with it, for example it has a value: Double property. Consider then the SilverCoin type: is it a subtype of Coin? Well, if the only interface is the value: Double property, then yes it is, it still has a value, and maybe it adds more (for example carats: Double).
So, SilverCoin is a subtype of Coin.
Now, say that I have I Purse<Coin>, that has a certain interface, that is, I can do something with it. The question is: is Purse<SilverCoin> a subtype of Purse<Coin>? One could say that it's natural to think so, but it depends on what I can do with a Purse<Coin>. If I can add any kind of coin to Purse<Coin>, then it's pretty much not a supertype of Purse<SilverCoin>, because I cannot do the same with the latter.
Swift warns against casting to unrelated types, and rightly so. But it doesn't warn against casting [Int] to [Any], even if, strictly speaking, they are not related by a subtype relationship in the general case.
It's still an assignment though. The code in your example changes the instance from C to B: it's behind the scenes, but it does so, it's just hidden from the user.
What I'm getting at (admittedly, in a pretty convoluted way in my original code) is that Swift generally warns against casting something to something else of unrelated type, but it doesn't warn against [Int] as [Any], and I was saying so really just in response to the OP, in order to show that it's not that the Woo example should be permitted: it's the Array example that shouldn't (again, in the general case).
And my point is that this is something that mutating operations are allowed to do, just as append on an Array might change the dynamic type of the array behind the scenes. If you have a variable of static type that might admit multiple subtypes dynamically, there should not necessarily be any expectation that the dynamic type remains the same after a mutating operation.
But as others have noted, these types aren't unrelated—an array of Ints really is (or can reasonably converted to) an array of Anys, since every Int is an Any. Yes, once you have a value of type [Any] it's possible to modify it in such a way that it is no longer an array of [Int]s, but this doesn't make the [Int] to [Any] conversion somehow unsound, nor should it be surprising that the [Any] to [Int] conversion subsequently fails—it is of course a cast that could always fail, just as ([""] as [Any]) as! [Int] will fail.
As with all value types, these are all conceptually "whole reassignments":
array[1] = 2
tuple.1 = 2
structWithTwoFields.second = 2
var value = 42
value.sign = -1
print(value) // -42
where:
extension Int {
var sign: Int {
get {
self < 0 ? -1 : 1
}
set {
if (newValue < 0) != (self < 0) {
self = -self
}
}
}
}
It's only with reference types it could be "partial assignment", and not always (as @Jumhyn example just showed).
As noted they are related as Any is similar to the base class in the class example. In fact you can construct your example without using Any. And without using Arrays FTM, as mentioned above tuples would expose exactly the same behaviour:
class MyAny {}
class Foo: MyAny {}
class Bar: MyAny {}
var x1: (Foo, Foo) = (Foo(), Foo())
var y1 = x1 as (MyAny, MyAny)
print(y1 as! (Foo, Foo)) // ok
y1.1 = Bar()
print(y1 as! (Foo, Bar)) // ok
print(y1 as! (Foo, Foo)) // ❌ runtime crash
I'm aware of what those words mean, and the fact that those words exist indicates that the purse example is not rigorous in itself, it's just too simplistic, and it doesn't go into the details of the variance of the relationship. It taps into the fact that it seems "reasonable" that a purse of silver coins can also be considered a purse of coins, but it depends on what you can do with it, and the variance of the relationship (that could as well be invariant, depending on the interface).
This is not really true though. More specifically, what's not true is the following implication:
(every Int is Any) implies (every [Int] is [Any]).
This implication would be true if the relationship from [Any] to [Int] was of covariant subtyping, but that's not true in the general case, because of functions with generic arguments in "consuming" position. In fact, if an Array had only those, the relationship between [Any] to [Int] would be contravariant, that is, [Any] would be a subtype of [Int], and not viceversa (because I can always append an Int to [Any]).
There is a difference between "really is" and "it can be reasonably converted to", and deals with what @John_McCall mentioned here:
I 100% agree that for most users it can be reasonably expected that [Int] should be considered a subtype of [Any], and in many actual use cases it makes sense (for example, when we only care about extracting values from the array).
And actually, in more general cases, the compiler could infer soundness for us. I mentioned it here:
Fair enough, I agree. The fact that there's an assignment involved is a detail of value types, but a mutating func is still part of the interface of a type, and subtypes (given a certain variance of generics parameters, if any) should be able to satisfy that interface for the relationship to be sound.
Casting to Any is always ok because Any has really no interface (so every other type can satisfy it). Casting [Int] to [Any] though, in rigorous terms, should be warned against, because of things like the append function, that would accept Any in [Any], but it cannot be satisfied by [Int]. In fact, if Array only had the append function, the relationship between [T] and [U] would be contravariant on the parameters, that is, if T: U, then [U]: [T].
Array achieves covariant subtyping outside the general case based on the compiler's specific knowledge of how array behaves with respect to assignments. Namely, the compiler knows that when an array is assigned to a value of a new type, the array is semantically copied so that it is valid to insert values of the supertype into the array without violating the constraints of the original (subtype) value.
The same sort conversion must happen for more 'direct' subtype relationships such as Int: Any. Here too the compiler performs a copy and potentially boxes the value in order to achieve the subtyping relationship (and as a previous commenter has noted, this happens on an elementwise basis when you perform the analogous operation on [Int]).
The failure you're describing is of a very different type than the classic Java array covariance failure:
String[] a = new String[4];
Object[] b = a;
b[0] = a; // crash!
Without value semantics, we do have a violation—bappears to be an Object[] but it can in fact fail to do something that an Object[] should be able to do. This failure mode doesn't exist in Swift, though. Every single step in your example behaves exactly as expected according to the types as defined (modulo the fact that there's no general mechanism to achieve generic covariance). I understand how it's a bit odd that the dynamic cast to [Int] fails, but in isolation it's entirely reasonable that [Any] as! [Int] might crash at runtime, since that's exactly what as! expresses.
It absolutely is the same. xs2 really is a brand new [Any], not just a type erased [Int]. This is supported by the documentation that @benrimmington posted earlier.