Type Erasure

A portrait painting style image of a pirate holding an iPhone.

by The Captain

on
June 11, 2023

Advanced Swift: Type Erasure

In Swift, generic types are widely used as they provide flexibility and reuseability to our code. However, sometimes we may face a problem where we need to abstract a type that conforms to a certain protocol without disclosing its exact underlying type. In such cases, we can use a pattern called type erasure.

Type erasure is a design pattern that allows us to create a concrete type which hides the original concrete type while still conforming to a protocol.

To understand this pattern better, let's consider a simple example. Here, we have a protocol called Shape which has a function for calculating its area:

protocol Shape {
  func calculateArea() -> Double
}

We also have two concrete types, a Rectangle and a Circle, which conform to the Shape protocol:

struct Rectangle: Shape {
  let length: Double
  let breadth: Double
  
  func calculateArea() -> Double {
    return length * breadth
  }
}

struct Circle: Shape {
  let radius: Double
  
  func calculateArea() -> Double {
    return Double.pi * radius * radius
  }
}

Now, let's say we want to create an array of Shape objects and calculate the total area of all shapes in the array:

let shapes: [Shape] = [Rectangle(length: 10, breadth: 5), Circle(radius: 5)]
var totalArea: Double = 0

for shape in shapes {
  totalArea += shape.calculateArea()
}

print(totalArea) // Output: 223.4131591025766}

So far, everything works fine. However, what if we want to create a new function which takes an array of Shape objects as a parameter and logs the area of each shape in the array:

func logAreaOfShapes(_ shapes: [Shape]) {
  for shape in shapes {
    print("Area of shape is \(shape.calculateArea())")
  }
}

logAreaOfShapes(shapes)}

Here, we encounter a problem. The function takes an array of Shape objects, but we cannot access the properties of the underlying concrete types, such as length and breadth for a Rectangle or radius for a Circle.

To solve this problem, we can create a type-erased wrapper which conforms to the Shape protocol. This wrapper can store the underlying concrete type and pass through the calculateArea() function to the original concrete type:

struct AnyShape: Shape {
  private let calculateAreaFunc: () -> Double
  
  init(_ shape: T) {
    calculateAreaFunc = shape.calculateArea
  }
  
  func calculateArea() -> Double {
    return calculateAreaFunc()
  }
}

Here, we have a new type called AnyShape which takes a generic type parameter and conforms to the Shape protocol. It stores the underlying concrete type's calculateArea() function in a closure which is then passed through by the AnyShape's own calculateArea() function.

With this wrapper, we can now modify the logAreaOfShapes() function to use the AnyShape type instead:

func logAreaOfShapes(_ shapes: [AnyShape]) {
  for shape in shapes {
    print("Area of shape is \(shape.calculateArea())")
  }
}

Now, we can convert our original Rectangle and Circle objects to AnyShape objects and pass them to the logAreaOfShapes() function:

let anyShapes: [AnyShape] = [AnyShape(Rectangle(length: 10, breadth: 5)), AnyShape(Circle(radius: 5))]
logAreaOfShapes(anyShapes)}

This will output the area of each shape:

Area of shape is 50.0
Area of shape is 78.53981633974483}

Summary: Type erasure is a design pattern that allows us to create a concrete type which hides the original concrete type while still conforming to a protocol. With the use of an AnyShape type, we were able to abstract our concrete types and pass them as generic parameter to a function.