around type erasure in swift
Jun 25, 2016WWDC 2016 being already a past event (with lots of exiting material to watch), I’ve finally found more time to look more carefully to one of the most important programming paradims promoted in Swift: Protocol-Oriented Pragramming. Protocol-Oriented Programming in Swift was a great talk at WWDC 2015, where the concept was beautfully explained and demonstrated by Dave Abrahams. During this year’s WWDC 2016 we have a follow-up: Protocol and Value Oriented Programming in UIKit Apps, which immediatelly invites us to watch again another great session from WWDC 2015: Building Better Apps with Value Types in Swift.
The protocol-oriented programming is a powerful concept and I still need to digest it and reflect on it trying to use it in our production code. Here we will take a short look at a technique called type erasure, which comes handy when dealing with generic protocols.
The problem
Say you have a protocol:
protocol Logger {
associatedtype LoggerItemType
func log(item: LoggerItemType)
}
and you have a type that uses it:
struct Proccess {
let logger: Logger
}
Unfortunately, this will fail with compile error saying: Protocol ‘Logger’ can only be used as a generic constraint because it has Self or associated type requirements.
The solution
This problem can be solved by applying so called Type-Erasure technique in which we wrap the generic protocol, Logger
in our case), in another type conforming to the same protocol (Logger
again) and delegating all requirements to an internal object. Such wrappers are often named Any{Protocol}
:
struct AnyLogger<LoggerItemType> : Logger {
let _log: LoggerItemType -> ()
init<BaseType: Logger where BaseType.LoggerItemType == LoggerItemType>(base: BaseType) {
_log = base.log
}
func log(item: LoggerItemType) {
_log(item)
}
}
struct IntLogger: Logger {
func log(item: Int) {
print("IntLogger: \(item)")
}
}
struct Process<T> {
let logger: AnyLogger<T>
}
let process = Process(logger: AnyLogger(base: IntLogger()))
process.logger.log(24) // prints '24'
This solves the problem but when Logger
protocol has many requirements, AnyLogger
has to bridge all of them. Also every time you extend the Logger
protocol, you need to update AnyLogger
as well.
We can improve this situation - at the cost of an extra indirection - by introducing a logger provider and applying type-erasure on it:
protocol LoggerProvider {
associatedtype LoggerType
func get() -> LoggerType
}
struct AnyLoggerProvider<LoggerType>: LoggerProvider {
let _get: () -> LoggerType
init<BaseType: LoggerProvider where BaseType.LoggerType == LoggerType>(base: BaseType) {
_get = base.get
}
func get() -> LoggerType {
return _get()
}
}
struct IntLoggerProvider: LoggerProvider {
let intLogger: IntLogger
func get() -> IntLogger {
return intLogger
}
}
struct Process<T> {
let loggerProvider: AnyLoggerProvider<T>
}
let process = Process(loggerProvider: AnyLoggerProvider(base: IntLoggerProvider(intLogger: IntLogger())))
process.loggerProvider.get().log(24)
We see that, now we got one extra intermediate call, but we do not have to duplicate the Logger
requirements in the wrapper.
Get the playground here: AroundTypeErasureInSwift.playground.