IOS Swift Opaque type

  • Prerequisite
  • The Problem That Opaque Types Solve
  • Opaque type v.s. Generic type
  • Opaque type v.s. boxed protocol type
  • Generics type、Opaque type、Boxed protocol type
  • Opaque type會保留原始type identity
  • SwiftUI運用Opaque type例子


Prerequisite

Swift提供兩種value type來隱藏實作細節:

  1. Opaque type
  2. Boxed protocol type

Note 1:
通常在module和client之間會隱藏type資訊,因為client端不需要知道module內的private type
Note 2:
Opaque types保留type identity(p.s. compiler能知道type資訊,但是client端不知道)
Boxed types沒有保留type identity(p.s. compiler不知道type資訊,直到runtime執行時才會知道)

例如:

protocol Shape {
associatedtype T
func draw() -> String
}
struct Triangle: Shape {
typealias T = Int
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 Square: Shape {
typealias T = Int
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")
}
}
// Opaque types
// Compile error: Conflicting arguments to generic parameter 'τ_0_0' ('Square' vs. 'Triangle')
var tmp1: [some Shape] = [Square(size: 1), Triangle(size: 1)] //[Square(size: 1), Square(size: 2)]
// Boxed types
var tmp2: [any Shape] = [Square(size: 1), Triangle(size: 1)]

The Problem That Opaque Types Solve

我們只在乎function回傳的結果是
The code inside the module could build up the same shape in a variety of ways, and other code outside the module that uses the shape shouldn’t have to account for the implementation details

例如:

protocol Shape {
associatedtype T
func draw() -> String
}
struct Triangle: Shape {
typealias T = Int
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 Square: Shape {
typealias T = Int
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")
}
}
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
struct JoinedShape<T: Shape, U: Shape>: Shape {
var top: T
var bottom: U
func draw() -> String {
return top.draw() + "\n" + bottom.draw()
}
}
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
}
// Below is client
let myShape = makeTrapezoid()
let drawResult = myShape.draw()
print(drawResult)

Opaque type v.s. Generic type

Generic type: Let caller pick the type for that function’s parameters and return value
Opaque type: The function implementation decides the return value’s type

Example of Generic type let caller decide the type of function’s parameters and return value:

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

Example of Opaque type decided by function implementation:

public protocol Shape {
associatedtype T
func draw() -> String
}
struct Triangle: Shape {
typealias T = Int
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 Square: Shape {
typealias T = Int
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")
}
}
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
struct JoinedShape<T: Shape, U: Shape>: Shape {
var top: T
var bottom: U
func draw() -> String {
return top.draw() + "\n" + bottom.draw()
}
}
public func makeTrapezoid() -> some Shape {
let top = Square(size: 3) // Triangle(size: 2)
let middle = Square(size: 2)
let bottom = FlippedShape(shape: top)
let trapezoid = JoinedShape(top: top,
bottom: JoinedShape(top: middle, bottom: bottom)
)
print(trapezoid.self) // the type is decided by what types you used to create JoinedShape
return trapezoid
}


Opaque type v.s. boxed protocol type

Opaque type:
在opaque type array內的每個element都是相同的type,且這個type遵守指定的protocol

Opaque type array example:

protocol Shape {
associatedtype T
func draw() -> String
}
struct Triangle: Shape {
typealias T = Int
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 Square: Shape {
typealias T = Int
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")
}
}
// Opaque types
// Compile error: Conflicting arguments to generic parameter 'τ_0_0' ('Square' vs. 'Triangle')
var tmp1: [some Shape] = [Square(size: 1), Triangle(size: 1)] //[Square(size: 1), Square(size: 2)]
// Boxed types
var tmp2: [any Shape] = [Square(size: 1), Triangle(size: 1)]

boxed protocol type:
在boxed protocol type array內的每個element可以是不同type,但每個element的type都遵守指定的protocol。

boxed protocol type array example:

public protocol Shape {
func draw() -> String
}
struct Triangle: Shape {
typealias T = Int
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 Square: Shape {
typealias T = Int
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")
}
}
struct VerticalShapes: Shape {
var shapes: [any Shape] // boxed Shape elements by adding any before the name of a protocol
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())


Generics type、Opaque type、Boxed protocol type

Contrast the three types you could use for shapes:

Using generics, by writing struct VerticalShapes 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.


Opaque type會保留原始type identity

當想要用protocol當return type時,如果protocol本身有associative type。此時,就無法使用protocol當return type。這時如果有使用Opaque type,則compiler不會跳出錯誤。

Opaque type retain type identity example:

public protocol Shape {
associatedtype T
func draw() -> String
}
class Triangle: Shape {
typealias T = Int
func draw() -> String {
return "Triangle"
}
}
// Boxed type
func makeTrapezoid1() -> any Shape {
let triangle = Triangle()
return triangle
}
// Opaque type retain type identity example
func makeTrapezoid2() -> some Shape {
let triangle = Triangle()
return triangle
}
// compile error: use of protocol 'Shape' as a type must be written 'any Shape'
func makeTrapezoid3() -> Shape {
let triangle = Triangle()
return triangle
}

Note:
Boxed type也會讓compiler不會跳出錯誤,只是boxed type不會保留type identity,所以直到runtime,我們才會知道value的type,且它能夠隨著時間改變。


SwiftUI運用Opaque type例子:

例子1:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world! (1)")
            Button("print my type") {
                print(type(of: self.body))
            }
            Text("Hello, world! (2)")
        }
        .padding()
    }
}

印出來的body type是:

ModifiedContent<VStack<TupleView<(Text, Button<Text>, Text)>>, _PaddingLayout>

例子2:

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
            Button("print my type") {
                print(type(of: self.body))
            }
            Image(systemName: "globe")
        }
        .padding()
    }
}

印出來的body type是:

ModifiedContent<VStack<TupleView<(Image, Button<Text>, Image)>>, _PaddingLayout>

Ref:

https://ejameslin.github.io/Opaque-return-types/

https://dsrijan.medium.com/opaque-type-in-swift-4015aeeca1ac

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/opaquetypes/

探索更多來自 LifeJourney 的內容

立即訂閱即可持續閱讀,還能取得所有封存文章。

Continue reading