Nesting Functions returning Protocol Types aren't "Invalid" in Swift 5.8

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