Swift Generic Coercion Misunderstanding

Swift generic coercion misunderstanding

The difference is that Array (and Set and Dictionary) get special treatment from the compiler, allowing for covariance (I go into this in slightly more detail in this Q&A).

However arbitrary generic types are invariant, meaning that X<T> is a completely unrelated type to X<U> if T != U – any other typing relation between T and U (such as subtyping) is irrelevant. Applied to your case, Signal<ChildClass> and Signal<BaseProtocol> are unrelated types, even though ChildClass is a subtype of BaseProtocol (see also this Q&A).

One reason for this is it would completely break generic reference types that define contravariant things (such as function parameters and property setters) with respect to T.

For example, if you had implemented Signal as:

class Signal<T> {

var t: T

init(t: T) {
self.t = t
}
}

If you were able to say:

let signalInt = Signal(t: 5)
let signalAny: Signal<Any> = signalInt

you could then say:

signalAny.t = "wassup" // assigning a String to a Signal<Int>'s `t` property.

which is completely wrong, as you cannot assign a String to an Int property.

The reason why this kind of thing is safe for Array is that it's a value type – thus when you do:

let intArray = [2, 3, 4]

var anyArray : [Any] = intArray
anyArray.append("wassup")

there are no problems, as anyArray is a copy of intArray – thus the contravariance of append(_:) is not a problem.

However, this cannot be applied to arbitrary generic value types, as value types can contain any number of generic reference types, which leads us back down the dangerous road of allowing an illegal operation for generic reference types that define contravariant things.


As Rob says in his answer, the solution for reference types, if you need to maintain a reference to the same underlying instance, is to use a type-eraser.

If we consider the example:

protocol BaseProtocol {}
class ChildClass: BaseProtocol {}
class AnotherChild : BaseProtocol {}

class Signal<T> {
var t: T

init(t: T) {
self.t = t
}
}

let childSignal = Signal(t: ChildClass())
let anotherSignal = Signal(t: AnotherChild())

A type-eraser that wraps any Signal<T> instance where T conforms to BaseProtocol could look like this:

struct AnyBaseProtocolSignal {
private let _t: () -> BaseProtocol

var t: BaseProtocol { return _t() }

init<T : BaseProtocol>(_ base: Signal<T>) {
_t = { base.t }
}
}

// ...

let signals = [AnyBaseProtocolSignal(childSignal), AnyBaseProtocolSignal(anotherSignal)]

This now lets us talk in terms of heterogenous types of Signal where the T is some type that conforms to BaseProtocol.

However one problem with this wrapper is that we're restricted to talking in terms of BaseProtocol. What if we had AnotherProtocol and wanted a type-eraser for Signal instances where T conforms to AnotherProtocol?

One solution to this is to pass a transform function to the type-eraser, allowing us to perform an arbitrary upcast.

struct AnySignal<T> {
private let _t: () -> T

var t: T { return _t() }

init<U>(_ base: Signal<U>, transform: @escaping (U) -> T) {
_t = { transform(base.t) }
}
}

Now we can talk in terms of heterogenous types of Signal where T is some type that's convertible to some U, which is specified at the creation of the type-eraser.

let signals: [AnySignal<BaseProtocol>] = [
AnySignal(childSignal, transform: { $0 }),
AnySignal(anotherSignal, transform: { $0 })
// or AnySignal(childSignal, transform: { $0 as BaseProtocol })
// to be explicit.
]

However, the passing of the same transform function to each initialiser is a little unwieldy.

In Swift 3.1 (available with Xcode 8.3 beta), you can lift this burden from the caller by defining your own initialiser specifically for BaseProtocol in an extension:

extension AnySignal where T == BaseProtocol {

init<U : BaseProtocol>(_ base: Signal<U>) {
self.init(base, transform: { $0 })
}
}

(and repeat for any other protocol types you want to convert to)

Now you can just say:

let signals: [AnySignal<BaseProtocol>] = [
AnySignal(childSignal),
AnySignal(anotherSignal)
]

(You can actually remove the explicit type annotation for the array here, and the compiler will infer it to be [AnySignal<BaseProtocol>] – but if you're going to allow for more convenience initialisers, I would keep it explicit)


The solution for value types, or reference types where you want to specifically create a new instance, to is perform a conversion from Signal<T> (where T conforms to BaseProtocol) to Signal<BaseProtocol>.

In Swift 3.1, you can do this by defining a (convenience) initialiser in an extension for Signal types where T == BaseProtocol:

extension Signal where T == BaseProtocol {
convenience init<T : BaseProtocol>(other: Signal<T>) {
self.init(t: other.t)
}
}

// ...

let signals: [Signal<BaseProtocol>] = [
Signal(other: childSignal),
Signal(other: anotherSignal)
]

Pre Swift 3.1, this can be achieved with an instance method:

extension Signal where T : BaseProtocol {
func asBaseProtocol() -> Signal<BaseProtocol> {
return Signal<BaseProtocol>(t: t)
}
}

// ...

let signals: [Signal<BaseProtocol>] = [
childSignal.asBaseProtocol(),
anotherSignal.asBaseProtocol()
]

The procedure in both cases would be similar for a struct.

Why can't Swift automatically convert a generic type parameter to its superclass?

Swift generics are invariant, meaning that two generic types are unrelated even if their generic type parameters have an inheritance relation. Invariant generic types cannot be cast to each other since they have no inheritance relation to each other.

This means that GenericStruct<B> and GenericStruct<A> are completely unrelated even if B is a subclass of A, hence you cannot cast one to the other.

Convert concrete type having generic in class signature swift

I'd suggest adding a new protocol

protocol StatusCodeProvider {
var statusCode: String? { get }
}

Add it as a requirement in your function making sure that T in NetworkResult<T> conforms to StatusCodeProvider and add conformance for every T you want to request.

Use Type Erasure return Generic Type in a function with Swift (Cannot convert return expression of type…)

EDIT to respond to the edit to the question:

createTwo doesn't work because you have the same misconception as I said in my original answer. createTwo decided on its own that F should be either String or Int, rather than "any type that conforms to Fooable".

For createOne, you have another common misconception. Generic classes are invariant. AnyFoo<String> is not a kind of AnyFoo<Fooable>. In fact, they are totally unrelated types! See here for more details.

Basically, what you are trying to do violates type safety, and you redesign your APIs and pick another different approach.


Original answer (for initial revision of question)

You seem to be having a common misconception of generics. Generic parameters are decided by the caller, not the callee.

In createOne, you are returning anyFoo, which is of type AnyFoo<Int>, not AnyFoo<P>. The method (callee) have decided, on its own, that P should be Int. This shouldn't happen, because the caller decides what generic parameters should be. If the callee is generic, it must be able to work with any type (within constraints). Anyway, P can't be Int here anyway, since P: FooProtocol.

Your createOne method should not be generic at all, as it only works with Int:

func createOne() -> AnyFoo<Int> {
let anyFoo = AnyFoo(p: FooImpClass())
return anyFoo
}


Related Topics



Leave a reply



Submit