The `some` keyword doesn't work as I would have expected and I don't understand why

Given the following code snippet:

protocol Vehicle {
    associatedtype Chassis: Equatable
    var chassis: Chassis  { get }
}

struct Wheels: Equatable {}
struct Tracks: Equatable {}

struct Car: Vehicle {
    let chassis: Wheels
}

struct Tank: Vehicle {
    let chassis: Tracks
}

func makeVehicle() -> some Vehicle {
    return Tank(chassis: Tracks())
}

func makeOtherVehicle() -> some Vehicle {
    return Tank(chassis: Tracks())
}

func run() {
    let v = makeVehicle()
    let c = makeOtherVehicle()
    if (v.chassis == c.chassis) { // error

    }
}

Why do I get an error when trying to compare the chassis? Shouldn't the compiler know that the concrete types are the same and can be safely compared?

1 Like

The entire purpose of some is to ensure that the calling code does not know the concrete types.

3 Likes

Ok, then how is it any different from a type erasing struct AnyVehicle? I was under the impression that with some, if the opaque type exposes associated types, their identities are also maintained.

Differences:

  • Opaque types don’t expose the underlying type to the caller however the compiler does maintain their identity.
  • Type erasures such as Protocols erase the type so the compiler doesn’t know their identity and therefore a function can’t return a protocol with an associated type / Self requirement (Example: Equatable). That is where Opaque types shine.

Reason for the error:

Consider the following:

func makeVehicle() -> some Vehicle

Compiler keeps the identity of the return value based on the function that returned it. So it treats it as makeVehicle‘s Vehicle type.

So compiler treats 2 instances returned by makeVehicle as the same type. However 2 instances returned by 2 different functions (makeVehicle and makeAnotherVehicle) are treated like 2 different types. Hence the error.

Opaque type requirements:

Functions that return an opaque type must return the same type inside the function. If there are multiple return statements inside the function, then all of them must return the same type otherwise the compiler would throw a compilation error. This is what enables the compiler to deal with Self / associated types in spite of not exposing the underlying type to the caller.

Example 2:

protocol Shape : Equatable {
    var size : Int { get }
}

struct Circle : Shape {
    var size: Int
}

struct Square : Shape {
    var size: Int
}

func makeShape1(size: Int) -> some Shape {
    return Circle(size: size)
}

let s1 = makeShape1(size: 5)
let s2 = makeShape1(size: 10)

if s1 == s2 {
    print("equal")
}
else {
    print("not equal")
}
1 Like

That means it brings no value over existential protocol-as-type usage if the protocol has no Self or associated types, it makes sense. Thanks a lot for the detailed answer.

Also Opaque Types can be used in places where a concrete type is needed.

protocol P {}
struct S : P {}

func f1() -> some P {
    return S()
}

let p1 : P = S() //Protocol
let o1 = f1() //Opaque Type

//s parameter will accept only concrete types conforming to P
func f1<T>(s: T) where T : P {}

f1(s: o1) //Valid
f1(s: p1) //Error: Protocol type 'P' cannot conform to 'P' because only concrete types can conform to protocols
2 Likes