Skip to content

Latest commit

 

History

History
177 lines (147 loc) · 5.54 KB

generic.md

File metadata and controls

177 lines (147 loc) · 5.54 KB

Generic types

Opaque return type vs generics

Assuming we want to create a type that conforms to the Shape protocol, there's different ways to do it.

protocol Shape {
    func draw() -> String
}
  • Using generics, by writing struct VerticalShapes<S: Shape> and var shapes: [S], makes an array whose elements are some specific shape type, and where the identity of that specific type is visible to any code that interacts with the array.
  • Using an opaque type, by writing var shapes: [some Shape], makes an array whose elements are some specific shape type, and where that specific type’s identity is hidden.
  • Using a boxed protocol type, by writing var shapes: [any Shape], makes an array that can store elements of different types, and where those types’ identities are hidden.

A boxed protocol type is also sometimes called an existential type, which comes from the phrase “there exists a type T such that T conforms to the protocol”. To make a boxed protocol type, write any before the name of a protocol.

struct VerticalShapes: Shape {
    var shapes: [any Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}


let largeTriangle = Triangle(size: 5)
let largeSquare = Square(size: 5)
let vertical = VerticalShapes(shapes: [largeTriangle, largeSquare])
print(vertical.draw())

In the example above, VerticalShapes declares the type of shapes as [any Shape] — an array of boxed Shape elements. Each element in the array can be a different type, and each of those types must conform to the Shape protocol. To support this runtime flexibility, Swift adds a level of indirection when necessary — this indirection is called a box, and it has a performance cost.

protocol AnyShape {
    func draw() -> String
}

protocol Shape: AnyShape, Equatable {
    //
}

struct Square: Shape {
    let size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let spaces = String(repeating: " ", count: size - 2)
        
        return """
        \(line)
        *\(spaces)*
        *\(spaces)*
        \(line)
        """
    }
}

struct Triangle: Shape {
    func draw() -> String {
        """
           *
          * *
        *     *
        *******
        """
    }
}

struct Merge2<T: Shape, U: Shape>: Shape {
    let first: T
    let second: U
    
    init(_ first: T, _ second: U) {
        self.first = first
        self.second = second
    }
    
    func draw() -> String {
        "\(first.draw())\n\(second.draw())"
    }
}

struct Diamond: Shape {
    func draw() -> String {
        """
           *
          * *
         *   *
          * *
           *
        """
    }
}

struct Flip<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        "\(shape.draw().reversed())"
    }
}

// Opaque : when you want the function to decide the return type
// Generic: when you want the caller to decide the return type

enum Shapes {
    // The caller of this method, still thinks it's getting a Shape
    // Why not just return Merge? It'll expose types we don't want to.
    // We lose the ability to change Merge to be another type in the future.
    // Also, we'll lose the ability to run some operations such as == since it needs to know the
    // type of the left and right side of the operator.
    static func merge2<T: Shape, U: Shape>(_ first: T, _ second: U) -> some Shape {
        // An important proviso here is that functions with opaque return types must always return
        // one specific type.
        // The body of this function knows exactly the return type, which is Merge2.
        let merged = Merge2(first, second)
        return merged
    }
    
    // Not acceptable : static func flip(_ shape: any Shape) -> any Shape
    // Also acceptable: static func flip<T: Shape>(_ shape: T) -> any Shape {
    static func flip(_ shape: some Shape) -> any Shape {
        if shape is Square {
            // Using return tome `some Shape` would throw an error because it sees that the body
            // can potentially return two different types.
            
            // Error:
            // Function declares an opaque return type 'some Shape', but the return statements in
            // its body do not have matching underlying types
            return shape
        } else {
            return Flip(shape: shape)
        }
    }
    
    static func random<T: Shape, U: Shape>(_ first: T, _ second: U) -> any Shape {
        [first, second].randomElement()!
    }
}

let square = Square(size: 2)
let triangle = Triangle()
let diamond = Diamond()
let mergedOne = Shapes.merge2(square, triangle)
let mergedTwo = Shapes.merge2(Square(size: 4), triangle)

// This wouldn't be possible with `any Shape`
// Opaque types solve this problem because even though we just see a protocol being used,
// internally the Swift compiler knows exactly what that protocol actually resolves to
mergedOne == mergedTwo

// By using generics, the caller can decide the type.
let flip: AnyShape = Shapes.random(square, triangle)