In the document, the Chapter "Opaque Types" describes the difference between protocol types and opaque types as function return types as follows:

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(smallTriangle)) that applies multiple transformations is invalid because the flipped shape isn’t a valid argument to protoFlip(_:).

However, in practice, unlikely, it is valid as proven by the following example:

#if swift(>=5.8)
print("hello swift 5.8+")
#endif

protocol Shape {
    func draw() -> String
}

struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result: [String] = []
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        if shape is Square {
            return shape.draw()
        }
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}

struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array<String>(repeating: line, count: size)
        return result.joined(separator: "\n")
    }
}

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}

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

let smallTriangle = Triangle(size: 3)
let nestedThing = protoFlip(protoFlip(smallTriangle))
print(nestedThing.draw())
// hello swift 5.8+
// *
// **
// ***

What do you think about it?
Can it be a bug of the compiler?
Originally, it doesn't make sense that "However, a value of a protocol type doesn’t conform to that protocol."

Thanks!

I believe the description is outdated. Since introduction of SE-0352, any P can be passed as an argument of generic function, and the pointed behavior seems to be due to that change.

3 Likes

Yeah, seems outdated / overlooked. With the small mention that there are situations when the existential can't be opened, in which case you can nest flip but not protoFlip, e.g.:

func flip<T: Shape>(_ shapes: [T]) -> [some Shape]
...
func protoFlip<T: Shape>(_ shapes: [T]) -> [Shape]
...

// OK, compiles
flip(flip([smallTriangle]))
// Not OK: Type 'any Shape' cannot conform to 'Shape'
protoFlip(protoFlip([smallTriangle]))

I'm not sure if the documentation implies the latter case, but it worths mentioning.

2 Likes