Opaque Type
A function with an opaque type hides its return value’s type information. Hiding type information at some boundaries between a module and code that calls into the module. Unlike returning a value whose type is a protocol type, opaque type preserve type identity —the compile has access to the type information, but clients of the module don’t.
The Situation
Here we have a Shape
protocol.
protocol Shape { func draw() -> String}
The struct Triangle
conform to the Shape
. Describe how to draw()
.
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: 4)print(smallTriangle.draw())// Print:// *// **// ***// ****
The struct FlippedShape
conform to the Shape
and it need an injection in a type of Shape
.
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 filppingShape = FlippedShape<Triangle>(shape: smallTriangle)print(filppingShape.draw())// Print:// ****// ***// **// *
The JoinedShape
conform to the Shape
and it need two injection in type of Shape
. It use the generic type T
and U
.
struct JoinedShape<T: Shape, U: Shape>: Shape{ var top: T var bottom: U func draw() -> String { return top.draw() + "\n" + bottom.draw() }}let joinedShape = JoinedShape(top: smallTriangle, bottom: filppingShape)print(joinedShape.draw())// Print:// *// **// ***// ****// ****// ***// **// *
Returning an Opaque Type
The opaque type like being the reverse of a generic type.
In generic, the function return a type that depends on its caller:
func max<T>(_ x: T, _ y: T) -> where T: Comparable {...}
Use opaque type. It return a some
type and don’t exposing the underlying type of that shape. It only focus on the return type, not the specific type.
struct Square: Shape { var size: Int func draw() -> String { let line = String(repeating: "*", count: size) let result = Array<String>(repeating: line, count: size) count: <#T##Int#>) 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}// Here we get a trapezoid, it's something that conform to the Shape and we can only use it as shape, the client can't access the underlying information of this shape.let trapezoid = makeTrapezoid()print(trapezoid.draw())// Print:// *// **// **// **// **// *
Combine Opaque Return Type with Generics.
func flip<T: Shape>(_ shape: T) -> some Shape { return FlippedShape(shape: shape)}func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape { return JoinedShape(top: top, bottom: bottom)}let opaqueJoinedTriangle = join(smallTriangle, flip(smallTriangle))print(opaqueJoinedTriangle.draw())// Print:// *// **// ***// ****// ****// ***// **// *
All the possible opaque return in a function must have the same type.
// Here is an example in error func invalidFlip<T: Shape>(_ shape: T) -> some Shape { if shape is Square { return shape } return FlippedShape(shape: shape)}
One way to avoid return different type is to move this Square
case into the FlippedShape
implementation.
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") }}
Using generics in an opaque return type.
func repeatObj<T: Shape>(shape: T, count: Int) -> some Collection { return Array<T>(repeating: shape, count: count)}
Differences Between Opaque Types and Protocol Types
Using protocol type
It can return different type that conform to Shape
, it makes a much looser API than opaque return type make.
// Protocol typefunc protoFlip<T: Shape>(_ shape: T) -> Shape { if shape is Square { return shape } return FlippedShape(shape: shape)}
The less specific return type information means that the operation that depends on type information aren’t available on the return value.
let protoFlipTriangle = protoFlip(smallTriangle)let sameThing = protoFlip(smallTriangle)print(protoFlipTriangle == sameThing) // Error they are 'Shape', 'Shape' has no func to check if they are equal, operator '==' cannot be applied to two 'Shape'
The opaque types preserve the identity of the underlying type.
protocol Container { associatedtype Item var count: Int { get } subscript(i: Int) -> Item { get }}extension Array: Container {}
Here we:
- can’t use
Container
as the return type of a function. Because the protocol has an associated type. - And can’t use it as constraint in a generic return type. Because there isn’t enough information outside the function body to infer what the generic type needs to be.
// 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, it has associate typefunc makeProtocolContainer<T, C: Container>(item: T) -> C { return [item]}
Using the opaque type some Container
as a return type. It means that the function return a container, but declines to specify the container’s type.
func makeOpaqueContainer<T>(item: T) -> some Container { return [item]}let opaqueContainer = makeOpaqueContainer(item: 12)let twelve = opaqueContainer[0]print(type(of: twelve)) // Int