Why can't I cast `Woo<Any>` to `Woo<Int>` concrete type? But `Array<Any>` to `Array<Int>` is OK?

In Swift, we can cast a type of Array<Any> to Array<Int> if the underlying type is in fact a Array<Int>:

let anyArray = [1,2,3] as Array<Any> 
let arrayActual = anyArray as! Array<Int> // Ok

But if I try the same thing with my own generic types, I get a warning from the compiler: Cast from 'Woo<Any>' to unrelated type 'Woo<Int>' always fails and a runtime crash:

struct Woo<Boo> {
    let boo: Boo
}

let anyWoo = Woo(boo: 3) as Woo<Any>
let wooActual = anyWoo as! Woo<Int> // Crash

What am I missing here? Why can I do this with Array, but not with my own types? Is there any way for me to do this?

The Swift compiler has special support for casting the built in collection types like Array. It's not yet a general feature.

8 Likes

Both would crash, it's just Array has a special treatment to treat arrays of T as arrays of Any and vice versa and there is no such special treatment for converting between Woo<Any> and Woo<T>.

Would the following work for you?

let anyWoo: Any = Woo(boo: 3)
let wooActual = anyWoo as! Woo<Int>
print(wooActual)
1 Like

Ah, ok. Makes sense. Thank you.
This seems like it would be handy to have as a general feature. Although, for my specific use case it's a workaround for not yet having variadic generics.

1 Like

Ok, makes sense. Thank you.

Hmm, I'm not sure. I suspect it might. I'll play around with this idea. Thank you!

Your use case doesn't actually require the associated type, but it's nice to have the option.

protocol JamesRandiProtocol<Boo> {
  associatedtype Boo
  var boo: Boo { get }
}

extension Woo: JamesRandiProtocol { }

(Woo(boo: 3) as Any) as! Woo<Int>
(Woo(boo: 3) as any JamesRandiProtocol<Int>) as! Woo<Int>
(Woo(boo: 3) as any JamesRandiProtocol) as! Woo<Int>

To reiterate what's been said or suggested elsewhere, we need this:

(Woo(boo: 3) as any Woo) as! Woo<Int>
2 Likes

Would members on any Woo that don't involve the associated type in their signatures be callable here? I'm thinking of things like count on Array that work the same regardless of what the array's element is.

Interesting... I'll play around with this idea. Thanks!

Yes. That's how things work already, with protocols:

func ƒ(collection: any Collection) -> (Any?, Int) {
  (collection.first, collection.count)
}

func ƒ<Collection: Swift.Collection>(collection: Collection) -> (Collection.Element?, Int) {
  (collection.first, collection.count)
}

some and any would be useful with all generic types—just, unlike with protocols, specifying the "associated type" would not useful, because unlike some/any Protocol<T>, there's nothing else for that to mean, other than Type<T> itself.

There hasn't been a lot of discussion yet. You all should start clamoring for it.

In the general case Type<Generic> should not be castable to Type<Any>, but the Swift compiler could be smart enough to understand, for example, that Woo<Boo> specifically could actually be cast to Woo<Any> without issues (I'm not saying that it should though, I personally think it's not worth it). What's missing is variance of generic types: the generic parameter model in Swift is invariant.

And "invariant" is actually, usually, the safest bet. For example, there's special support for Array, but Array should strictly be invariant. The fact that you can cast [Generic] to [Any] makes the following unsound code compile:

var xs1 = [1, 2, 3]
type(of: xs1) // Array<Int>.Type

var xs2 = xs1 as [Any]
type(of: xs2) // Array<Any>.Type

xs2 as! [Int] // works

xs2.append("yello")
type(of: xs2) // Array<Any>.Type

xs2 as! [Int] // CRASH
1 Like

There is nothing unsound about the covariant cast from [Int] to [Any], though. It’s the forced, dynamic contravariant cast that fails, because the dynamic types don’t match; this is consistent with force casts in general.

4 Likes

It's not as unsound as it looks. var xs2 = xs1 as [Any] is essentially the same as var xs2 = xs1.map { $0 as Any }, except IIRC the conversion might be lazy. There's definitely compiler magic involved, I just don't remember how magical the cast is.

as! crashes in order to prevent the unsoundness of treating an Array with both Strings and Int in it as an array of Ints. If you used xs as? [Int] you'd get nil instead.

1 Like

It’s not unsound at all. There’s no basic guarantee of the type system that’s violated here. You’re explicitly asking whether all the elements can be cast to a particular type, and of course the answer can be “no”.

7 Likes

The Dynamic Casting Behavior document has an Array/Set/Dictionary Casts section.

3 Likes

This deserves a longer post.

A language is sound if it doesn't accept programs that it shouldn't. "Shouldn't" is doing a lot of work there, though: it's an inherently open concept, reliant on an external semantic model. In programming languages, we generally say a program shouldn't be accepted if it "won't work", but our definition for "work" is that individual operations will behave like they're expected to, not that the program as a whole will do what the programmer meant for it to do. The latter definition, of course, can't be applied without pulling the user's intent into the semantic model, and any sort of bug in the program (or flaw in the user's understanding) would become an "unsoundness" in the language. (For example, if I wrote a Fibonacci-style function that started with 3,4,..., the language would be unsound if I meant it to be the standard Fibonacci sequence.) In the more standard PL approach, the language designer just has to rigorously define how individual operations are supposed to work.

One way to do that is to give well-defined semantics to all possible combinations of operands, even ones that don't a priori make much sense. This is common in dynamically-typed languages; for example, JavaScript's subscript operator is well-defined even if you use it on an integer. In statically-typed languages, we usually rule out a lot of those cases by defining static preconditions on operations. For example, in Swift, the subscript syntax requires the base operand to statically have a subscript member, and then the dynamic semantics impose a precondition that the base value actually has the type it was type-checked to have. In that world, the language is formally sound as long as it's correctly enforcing that those static preconditions on individual operations can't be violated.

Sometimes we do use "soundness" in a slightly less formal sense: we argue about how users expect basic operations to work. In this informal sense, a language can be unsound if it violates those user expectations even if dynamically everything remains well-defined. For example, Java has covariant object arrays: you can implicitly convert a String[] to Object[], but assignments into the result will fail with an exception if they aren't dynamically Strings. This is not formally unsound in Java because this aspect of the well-typedness of the array assignment syntax is not a static precondition; it's a well-defined dynamic check. Nonetheless, it is very arguably unsound behavior under a higher-level understanding of how assigning into an array element ought to work, because a bunch of things that the language accepts implicitly and without complaint can be combined to dynamically fail with a type error.

But I have a hard time accepting that that could ever apply to dynamic cast. Dynamic casts are an explicit syntax whose entire purpose is to dynamically check whether a value has a particular type. The possibility that that can fail when values don't have that type is inherent to the operation.

15 Likes

If you'd like to stop

xs2.append("yello")

from being possible in that example, I think that what you'd really want is some Array or any Array, not [Any]. You can see the underpinnings of the restrictions that would be applied when Element is missing:

var xs2: some RangeReplaceableCollection = xs1
xs2.append(4) // Cannot convert value of type 'Int' to expected argument type '(some RangeReplaceableCollection).Element'
var xs2: any RangeReplaceableCollection = xs1
xs2.append(4) // No exact matches in call to instance method 'append'
3 Likes

Much simpler version of your code:

var x: Any = 1
x as! Int    // ok
x = "yello"
x as! Int    // crash

As pointed out above – nothing wrong here.

3 Likes

I don't think that captures the spirit of it, which to me, is, "type erase to Array".

The spirit of your example, following, would be just, "type erase". But there's no meaningful mutation you can make to a "some Any", so it might as well just be a constant.

var x: some Any = 1
x as! Int // 1
x = "yello" // Cannot assign value of type 'String' to type 'some Any'

I went to sleep after that last post, and woke up thinking, "Wait, would that form possibly work for your actual use case?".

let array = [1,2,3]
let anyArray: [some Any] = array
let arrayActual = anyArray as! [Int] // 😶👍

struct Woo<Boo> { let boo: Boo }
let woo = Woo(boo: 3)
let anyWoo: Woo<some Any> = woo
let wooActual = anyWoo as! Woo<Int> // 😶👍
var xs2: [some Any] = [1, 2, 3]
xs2 as! [Int] // 😶👍
xs2.append("yello") // No exact matches in call to instance method 'append'

You still can't do as some though; you need the extra line.

2 Likes

That's really not. In my code, I'm not casting something to Any, reassigning the whole thing, and then force casting it back to something unrelated. I'm exploiting the fact that the compiler accepts [Any] as a supertype of [Int], but that's not strictly true, because not all operations that I can do on [Any] can be done on [Int]. That's why I say that it's unsound.

Consider this code:

class Foo {
  func frobulate() {}
}

class Bar {
  func frobnicate() {}
}

class Baz: Bar {}

var x = Baz()

var y = x as Bar // No issue

var z = x as Foo // Cast from 'Baz' to unrelated type 'Foo' always fails

The compiler doesn't complain on let y = x as Bar, and it shouldn't because everything that can be done with a Bar can also be done with a Baz, that's the point of subtyping (please note that this is unrelated to the Liskov substitution principle, that's often thrown around: that deals with behavioral [i.e. semantics] subtyping, but that's not the point here, it's really just about being able to operate on the same interface).

Consider this then:

var xs = [1, 2, 3]

var ys = xs as [Any] // No issue

var zs = xs as [String] // Fail

The compiler accepts the line var ys = xs as [Any], but it really shouldn't, because there are some things that I can call on a mutable [Any] that I cannot call on a mutable [Int]. The runtime, as far as I know, solves the conundrum by dynamically modifying the internals of the array instance: that's what I actually wanted to show with my original code, that something is happening at runtime in the background that produces unexpected results.

This is solved with the example from @anon9791410 that uses the more precise some RangeReplaceableCollection and any RangeReplaceableCollection.

It's not the same because, semantically, in the map case you're creating a new instance of the array, so there's no reason to consider it related to the original instance at the type level.

2 Likes