swiftopaque-types

Swift Opaque Type vs Protocols - documentation infers protocol's func can't nest


On reading Apple's Swift Programming Language guide regarding opaque types, there's one paragraph I don't understand. The guide is discussing the differences between opaque types and protocols, and states that you can't nest calls that return a protocol type. They use this code fragment, where Shape is a protocol:

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }

    return FlippedShape(shape: shape)
}

It then states that:

Another problem with this approach is that the shape transformations don’t nest. The result of flipping a triangle is a value of type Shape, and the protoFlip(:) function takes an argument of some type that conforms to the Shape protocol. However, a value of a protocol type doesn’t conform to that protocol; the value returned by protoFlip(:) doesn’t conform to Shape. This means code like protoFlip(protoFlip(smallTriange)) that applies multiple transformations is invalid because the flipped shape isn’t a valid argument to protoFlip(_:).

However, I wrote this code:

import Foundation

protocol P {
    associatedtype  AT
}


struct C: P {
    typealias AT = Int
}

func f<T: P>(_ t: T) -> T {
    t
}

func g() {
    f(f(C()))
}

g()

and this compiles and runs...and appears to let me nest those calls.

What am I mis-understanding? What is the documentation trying to say?


Solution

  • You wrote this:

    func f<T: P>(_ t: T) -> T
    

    This takes and returns the same type.

    That's not the problem. The problem is the example:

    func protoFlip<T: Shape>(_ shape: T) -> Shape
    

    This takes a T and returns a Shape existential.

    This would be equivalent to:

    func f<T: P>(_ t: T) -> P
    

    (Takes a T and returns a P existential.)

    If you wrote that, you'd find you have the issue described. You cannot pass a P existential into f() because protocol existentials do not conform to their protocol in Swift. (This is due to various corner-cases it can create due to static methods and initializers. Rather than deal with the corner-cases, today protocol existentials just do not conform to their protocol. This may change in the future for the cases where it could be allowed but isn't currently.)

    Opaque types allow you to write:

    func f<T: P>(_ t: T) -> some P
    

    Rather than returning a P existential, this returns a concrete (but opaque) type that conforms to P. The system tracks this as "the type that is returned by f when parameterized by T." It is not equivalent to any other some P, but it is concrete, it is known at compile time, and it can be passed into f().