WWDC 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.

Further Reading

  1. A Little Respect for AnySequence
  2. StackOverflow. Anwer to question: How to use generic protocol as a variable type?