Type erased generic usage

hello,

i am trying to express the following "type erased" structs in swift, but getting the Cast from 'Some<Int>' to unrelated type 'Some<Any>' always fails warning and it crashes indeed. how to do this right and what is the reason for Some<Int> not being type compatible with Some<Any>?

struct Some<T> {
    var v: T
}

let someInt = Some(v: 0)
let someString = Some(v: "hello")
let i = someInt as! Some<Any> // crashes
let s = someString as! Some<Any> // crashes
let someThings: [Some<Any>] = [i, s]

Generic parameters are invariant w.r.t. their containers. So Some<Int> is unrelated to Some<Any>*. You'd need to reconstruct new Some with type-erased T.

Some(v: someInt.v as Any)

* There're some exceptions baked in to the compilers, e.g. Array, Dictionary.

3 Likes

Here is something that might help:

extension Some {
    func upcasted() -> Some<Any> { Some<Any>(v: v) }
}

extension Some where T == Any {

    func upcasted() -> Some<Any> { self }

    func downcast<U>(to targetType: U.Type) -> Some<U>? {
        guard let v = v as? U else { return nil }
        return Some<U>(v: v)
    }
}

let someInt = Some(v: 0)
let someString = Some(v: "hello")
let i = someInt.upcasted() // works
let s = someString.upcasted() // works
let someThings: [Some<Any>] = [i, s]

print(someThings.map { $0.v }) // works
print(someThings[0].downcast(to: Int.self)!.v) // works
print(someThings[1].downcast(to: String.self)!.v) // works
2 Likes

that works, thanks.

out of curiosity, what harm would it be for swift to treat Some<Int> as type related to Some<Any> ?

let someAny = someInt as Some<Any>
let someInt2 = someAny as! Some<Int>

i'm also using var procs: [(Some<T>) -> Void]
which i sometimes want to type erase to: var procs: [(Some<Any>) -> Void]

here again the types are unrelated and swift wants a more cumbersome code instead of a mere type cast.

Depending on what you do with T, it doesn't make sense in general. If you only have a getter of T:

extension Some {
  var value: T { ... }
}

then you can't convert Some<Any> to Some<Int> because value could return something other than Int. OTOH, if you have a setter:

extension Some {
  func set(value: T) { ... }
}

then you can't convert Some<Int> to Some<Any> because value because you could supply "some string" to Some<Any>.set, but you can't do that to Some<Int>.set.

In general, generic parameter is invariant w.r.t. to its container. You'd need a much stronger information to have Some<Any> be subtype or supertype (or both) of Some<Any>, which isn't possible to express in Swift right now (though is theoretically possible).

Even if you make sure that one is a variant of another, the type layout of Some<Any> and Some<Int> isn't guaranteed to be compatible, so you can't just reinterpret that portion memory as different generic. You'd need to do the conversion (again, not available in Swift).

1 Like

your examples are understandable, but i wouldn't want anything silly, like treating Some<Int> the same way as Some<String> via the intermediate typecast to Some<Any>. naturally the inappropriate calls to getters / setters in your above example would crash and that's expectable. i was only after the roundtrip Some<T> -> Some<Any> -> Some<same T>, obviously if i tried to cast it to Some<different Type> i would expect it not to work correctly.

true. although they managed to do that [Int] is type related to [Any]...

let intArray: [Int] = [1, 2, 3]
let anyArray: [Any] = intArray
let intArray2: [Int] = anyArray as! [Int]
let doubleArray: [Double] = anyArray as! [Double] // expected crash

and this is actually:

let intArray: Array<Int> = ...
let anyArray: Array<Any> = intArray
let intArray2: Array<Int> = anyArray as! Array<Int>
let doubleArray: Array<Double> = anyArray as! Array<Double> // expected crash

so the question is, what special "Array" has that my "Some" type doesn't, is that baked into the compiler?

I mean, with the current type system, you can't express the relationship between Some<Int> and Some<Any>. The former could be subtype of the latter, the latter could be subtype of the former, but having both would be "silly". If you just want a round-trip, you could just cast the entire Some<Int> -> Any -> Some<Int>.

Technically they aren't. They just make a copy (and convert each element) every time you do the casting. It is possible to expand such capabilities to custom types, but AFAIK that area hasn't been explored yet. Unexpectedly, mere type casting isn't exactly a trivial task.

2 Likes

i see. i thought those casts to Any don't do anything with the code, just muting compiler complaints (similar to reinterpret cast in C++).

With the addition of some and any type erasure in 5.7 it would be nice to allow something like

struct Foo<t: Bar> {}
var barA: Foo<any Bar> = ...
var barB = Foo(...)
barA = barB

and operate with type erased generics.

This could allow for collections of heterogeneous generic implementations.
Something like:

let myFoos: [Foo<any Bar>] = ...

Or upcasting of generic objects.

Currently doing Foo<any Bar> fails with Type 'any Bar' cannot conform to 'Bar'

1 Like