๐ดType Erasure
A use case to use an opaque return type is to perform automatic type erasure. ๐ Sundell
// --------------------------------------------------
// โญ๏ธ generic protocol (with associated type)
// --------------------------------------------------
protocol Fetcher {
associatedtype Data
typealias ResultHandler = (Result<Data, Error>) -> Void
func fetch(completion: ResultHandler)
}
// conforming type
struct User {
let id: Int
let name: String
}
// protocol conformance
struct UserFetcher: Fetcher {
typealias Data = User
func fetch(completion: ResultHandler) {
let user = User(id: 1, name: "Phil")
completion(.success(user))
}
}
/*
โญ๏ธ The Problem
--------------
How do we hold a reference to an object that has
implemented this protocol?
*/
//struct SomeStruct {
// let fetcher: Fetcher
//}
// โ Protocol โFetcherโ can only be used as a generic constraint
// because it has Self or associated type requirements
// ๐ฌ The compiler CANNOT determine the associated type `Data`
// from the declaration.
// ------------------------------------------------
// Type Erasure: turn PAT into generic type
// ------------------------------------------------
// 1. define a generic type conforming to protocol
struct AnyFetcher<T>: Fetcher {
// 2. turn associated types into type parameters.
typealias Data = T
// 3. create members with matching signatures.
// (for each member in the protocol)
private let _fetch: (ResultHandler) -> Void
// 4. erase the type of injected instance (โญ๏ธ)
// by storing a reference to all of the methods WITHOUT
// storing a reference to the injected instance (โญ๏ธ).
// โญโโโ โญ๏ธ โโโโฎ
init<F: Fetcher>(_ fetcher: F) where F.Data == T {
// store reference to methods, not the object.
_fetch = fetcher.fetch
}
// 5. forward all protocol method calls to stored references
func fetch(completion: ResultHandler) {
_fetch(completion)
}
}
// ----------------
// use case
// ----------------
// turn `UserFetcher` into `AnyFetcher` (erase `UserFetch` type)
let fetcher = AnyFetcher(UserFetcher())
// let it do fetch
fetcher.fetch { result in
switch result {
case .success(let user) : print(user.name)
case .failure(let error): print(error)
}
}
๐ Ray โฉ Enter Type Erasure
import SwiftUI
/// protocol with Self requirement
public protocol Filter:
Equatable, // with `Self` requirement
Identifiable
{
var name: String { get }
func apply(to image: Image) -> Image
}
// -------------------------------------------------
// Type Erasure: turn PSR into nongeneric type
// -------------------------------------------------
// โข PSR: protocol with `Self` requirement
// 1. define a nongeneric type conforming to protocol
public struct AnyFilter: Filter {
// 2. create members with matching signatures.
// (for each member in the protocol)
public let name: String
private let _apply: (Image) -> Image
// 3. erase the type of injected instance (โญ๏ธ)
// by storing a reference to all of the methods WITHOUT
// storing a reference to the injected instance (โญ๏ธ).
// โญโโ โญ๏ธ โโโโฎ
public init<F: Filter>(_ filter: F) {
name = filter.name
_apply = filter.apply(to:)
}
}
extension AnyFilter {
// `Filter` protocol conformance
// 4. forward all protocol method calls to stored references
public func apply(to image: Image) -> Image {
return _apply(image)
}
// `Equatable`
public static func == (lhs: AnyFilter, rhs: AnyFilter) -> Bool {
lhs.id == rhs.id
}
// `Identifiable`
public var id: String { "AnyFilter(\(name))" }
}
Ray โฉ Opaque Return Types and Type Erasure (๐ไพไบ) โญ๏ธ
Pacheco โฉ Type erasure in Swift: How to avoid it completely - use enum.
Last updated