swiftgenericsswiftuiswift-protocols

Are we able to nest functions that return protocol types now? Or was that always the case and the documentation is wrong?


I am working on a project with SwiftUI. And from the get-go, swiftUI shows the power and application of protocol oriented programming. So I started studying and expanding my knowledge on protocols and opaque types.

In the documentation The swift programming language towards the end of the page it clearly states:

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... when I try to nest the function, it works perfectly fine. So is the documentation wrong or is this a very recent update to the language and the documentation is a bit behind?

Here is the code (which I ran on playgrounds).

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")
    }

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


//Testing if nested function works
let smallTriangle = Triangle(size: 3)
let testNest = protoFlip(protoFlip(smallTriangle))

Solution

  • I think that the book is using a bad example to illustrate the problem. In general your code will compile and run correctly. But some swift features like associated types and self requirements in methods of protocol breaking the concept of polymorphism we used to.

    To summarise what I'am talking about take a look at this code:

    protocol Shape: Equatable {
        
        func draw() -> String
        
        func doSomething(with other: Self) // self requirement
    }
    
    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")
        }
        
        func doSomething(with other: Self) {
            print("Tri size: \(other.size)")
        }
    }
    
    
    struct FlippedShape<T: Shape>: Shape {
        var shape: T
        func draw() -> String {
            let lines = shape.draw().split(separator: "\n")
            return lines.reversed().joined(separator: "\n")
        }
        
        func doSomething(with other: Self) {
            print("Other filled shape: \(other.draw())")
        }
    }
    
    struct Rect: Shape {
        var width: Int
        var height: Int
        
        func draw() -> String {
            let line = String(repeating: "*", count: width)
            let result = Array<String>(repeating: line, count: height)
            return result.joined(separator: "\n")
        }
        
        func doSomething(with other: Self) {
            print("W: \(other.width) H:\(other.height)")
        }
    }
    
    func protoFlip<T: Shape>(_ shape: T) -> Shape { //Compiler emits error:  Use of protocol 'Shape' as a type must be written 'any Shape'
    
        return FlippedShape(shape: shape)
    }
    
    let smallTriangle = Triangle(size: 3)
    
    let protoFlippedTriangle = protoFlip(protoFlip(protoFlip(smallTriangle)))
    print(protoFlippedTriangle.draw())
    

    This code won't compile without opaque types because in this case compiler need to check if the shape we passed to functions is the same as we returned. Because we can not call doSomething on any other Shape other then the shape of the same type