The introduction

Continuing with the Swift documentation, from the previous section: Generics, we learned about the Swift protocol, mainly generics code that enables you to write flexible, reusable functions and types that can use any type you define for your needs. You can write code that avoids duplication and expresses its intent in a clear, abstract way. Now, let’s learn about Swift opaque types. Due to the long space, here is a section to record, next, let’s begin!

Opaque type

A function or method with an opaque return type hides the type information of its return value. Rather than providing a specific type as the return type of a function, the return value is described in terms of the protocol it supports. Hiding type information is useful at the boundary between the module and the code calling the module, because the underlying type of the returned value can remain private. Unlike a value whose return type is a protocol type, opaque types retain the type identifier, and the compiler has access to the type information, but the module’s client does not.

1 Problems solved by opaque types

For example, suppose you are writing a module that draws ASCII art graphics. The basic feature of ASCII art graphics is the draw () function, which returns a string representation of the shape, which can be used as a requirement of the shape protocol:

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") } } let smallTriangle = Triangle(size: 3) print(smallTriangle.draw()) // * // ** // ***Copy the code

You can use generics to do things like flip shapes vertically, as shown in the code below. However, this approach has one important limitation: the result of the rollover exposes the exact generic type used to create it.

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())
// ***
// **
// *
Copy the code

This way of defining the JoinedShape<T:Shape, U:Shape> structure connects the two shapes vertically, as shown in the following code, This causes types like JoinedShape< FlipedShape <\Triangle>, Triangle> to join the flipped Triangle to another Triangle.

struct JoinedShape<T: Shape, U: Shape>: Shape {
    var top: T
    var bottom: U
    func draw() -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
print(joinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *
Copy the code

Because full return types need to be declared, exposing details about shape creation allows types that are not part of the ASCII ART module’s public interface to leak out. Code inside a module can build the same shape in a variety of ways, and other code outside the module that uses that shape doesn’t have to worry about the implementation details of the transformation list. Wrapper types like JoinedShape and FlippedShape are not important to the user of the module, they should not be visible. The module’s common interface consists of operations such as join and flip shapes, which return another shape value.

2 Returns an opaque type

Opaque types can be thought of as the opposite of generic types. Generic types allow the code calling a function to choose a type for the function’s arguments and return values, which is abstracted from the function implementation. For example, a function in the following code returns a type that depends on the caller:

func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
Copy the code

The code that calls Max selects the values of x and y, and the types of these values determine the specific type of T. The calling code can use any type that complies with the Comparable protocol. The code inside the function is written in a generic way, so it can handle any type provided by the caller. The implementation of Max (: 🙂 uses only the functionality shared by all Comparable types.

For functions with opaque return types, these roles are reversed. Opaque types allow a function implementation to choose a type for the value it returns, which is abstracted from the code calling the function. For example, the function in the following example returns a trapezoid without exposing the underlying type of the shape.

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 makeTrapezoid() -> some Shape {
    let top = Triangle(size: 2)
    let middle = Square(size: 2)
    let bottom = FlippedShape(shape: top)
    let trapezoid = JoinedShape(
        top: top,
        bottom: JoinedShape(top: middle, bottom: bottom)
    )
    return trapezoid
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
// *
// **
// **
// **
// **
// *
Copy the code

The makedrapeoid () function in this example declares its return type as a shape; Therefore, the function returns a value of a given type that conforms to the shape protocol, without specifying any specific type. Writing Makedrapeoid () in this way allows it to express the basic aspects of its public interface, returning a value of a shape, without having to generate a specific type of shape made up of part of its public interface. This implementation uses two triangles and a square, but you can rewrite the function to draw trapezoids in various other ways without changing its return type.

This example highlights the way in which opaque return types are the opposite of generic types. Code in Makedrapeoid () can return any type it needs, as long as it conforms to the Shape protocol, just like code calling generic functions. The code that calls the function needs to be written in a generic way, such as the implementation of a generic function, so that it can handle any shape value returned by makedrapeoid ().

Opaque return types can also be used in conjunction with generics. The functions in the following code all return values of some type that conform to the shape protocol.

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}
func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
    JoinedShape(top: top, bottom: bottom)
}

let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
print(opaqueJoinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *
Copy the code

The value of opaqueJoinedTriangles in this example is the same as the value of joinedTriangles in the example of generics in the “problems solved by opaque types” section earlier in this chapter. However, unlike the values in this example, flip (:) and join (:) wrap the underlying types returned by the generic shape operation as opaque return types, which prevent them from being visible. Both of these functions are generic because the type they rely on is generic, and the type parameters of the function pass the type information required by FlippedShape and JoinedShape.

If a function with an opaque return type returns from more than one location, all possible return values must have the same type. For generic functions, the return type can use the function’s generic type argument, but it must still be a single type. For example, here is an invalid version of the shape flip function that contains the special case of a square:

func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
    if shape is Square {
        return shape // Error: return types don't match
    }
    return FlippedShape(shape: shape) // Error: return types don't match
}
Copy the code

If this function is called with a square, it returns the square; Otherwise, return to flip shape. This violates the requirement to return only one type of value and invalidFlip (:) code. One way to fix invalidFlip (:) is to move the special case of the square into the implementation of FlippedShape, which causes this function to always return the value of FlippedShape:

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")
    }
}
Copy the code

The requirement to always return a single type does not prevent you from using generics in opaque return types. Here is an example of a function that merges its type arguments into the underlying type of the value it returns:

func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
    return Array<T>(repeating: shape, count: count)
}
Copy the code

In this case, the basic type of the return value varies according to T: whatever shape is passed, repeat (Shape :count:) to create and return an array of that shape. However, the return value always has the same underlying type [T], so it follows the requirement that functions with opaque return types must only return values of a single type.

3 Differences between opaque types and protocols

The return opaque type looks very similar to a return type that uses a protocol type as a function, but the two return types differ in whether they retain the type identifier. Opaque types refer to a specific type, even though the function caller cannot see which type; The protocol type can refer to any type that matches the protocol. In general, protocol types provide more flexibility about the underlying types of the values they store, while opaque types allow you to make stronger guarantees about those underlying types.

For example, here’s a version of Flip (:) that uses the protocol type as its return type instead of the opaque return type:

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}
Copy the code

This version of the protoFlip (:) has the same body as flip (:) and always returns the same type of value. Unlike flip (:), the value returned by a protoFlip (:) does not always have to be of the same type; it just conforms to the Shape protocol. In other words, protoFlip (:) has a much looser API contract with the caller than flip (:) does. It retains the flexibility to return multiple types of values:

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

    return FlippedShape(shape: shape)
}
Copy the code

Depending on the shape passed in, the modified code returns either an instance of Square or an instance of FlippedShape. The two flip shapes returned by this function may have completely different types. When multiple instances of the same shape are flipped, other valid versions of this function may return different types of values. The less specific return type information in protoFlip (:) means that many operations that rely on type information are not available on the return value. For example, it is not possible to write a == operator to compare the results returned by this function.

let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing  // Error
Copy the code

The last line of the example is wrong for several reasons. The immediate problem is that shapes do not contain the == operator as part of their protocol requirements. The next problem you’ll encounter if you try to add one is that the == operator needs to know the type of its left and right arguments. Such operators typically take arguments of type Self, matching any specific type of the adopted protocol, but types that occur when adding the Self requirement to the protocol does not allow the protocol to be used as a type.

Using the protocol type as the return type of a function gives you the flexibility to return any type that conforms to the protocol. However, the price of this flexibility is that some operations cannot be performed on the returned value. This example shows how the == operator is not available — it depends on specific type information that is not stored by using protocol types.

Another problem with this approach is that shape transformations are not nested. The protocol type is a triangle, and its result is a triangle. However, the value of the protocol type does not match the protocol; ProtoFlip (:) returns a value that does not match Shape. This means that code similar to protoFlip (protoFlip (smallTriange)) that applies multiple conversions is invalid because the flipped shape is not a valid argument for protoFlip (:).

In contrast, opaque types retain the identity of the underlying type. Swift can infer the relevant type, which allows you to use opaque return values where the protocol type cannot be used as a return value. For example, here’s a version of the container protocol from generics:

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container { }
Copy the code

You cannot use the container as the return type of a function because the protocol has an associative type. Nor can you use it as a constraint in a generic return type, because there is not enough information outside the function body to infer what a generic type needs.

// Error: Protocol with associated types can't be used as a return type.
func makeProtocolContainer<T>(item: T) -> Container {
    return [item]
}

// Error: Not enough information to infer C.
func makeProtocolContainer<T, C: Container>(item: T) -> C {
    return [item]
}
Copy the code

The required API protocol uses the opaque type some Container as the return type. This function returns a Container but refuses to specify the type of the Container:

func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Prints "Int"
Copy the code

The type of twelve is inferred to be Int, which illustrates the fact that type inference works with opaque types. In the implementation of makeOpaqueContainer(item:), the underlying type of the opaque container is [T]. In this case, T is Int, so the return value is an array of integers, and the type of the Item association is inferred to be Int. The subscript on the Container returns Item, which means that the type of 12 is also inferred to be Int.

Reference Swift-Opaque Types