Skip to content

Instantly share code, notes, and snippets.

@danielt1263
Last active September 22, 2018 12:40
Show Gist options
  • Save danielt1263/0b5188eaa825b1239389b377d8cb23c1 to your computer and use it in GitHub Desktop.
Save danielt1263/0b5188eaa825b1239389b377d8cb23c1 to your computer and use it in GitHub Desktop.

Two Level Type Erasing in Swift 3

Recently I converted a project of mine to Swift 3 (https://github.com/dtartaglia/XStreamSwift) and I had to deal with a problem that I couldn't find an answer for. This article is about the problem and the solution I discovered.

There are lots of articles on the web about type erasing in Swift, but all the ones I found only dealt with a single level. I will recap the concept quickly:

protocol Listener
{
    associatedtype ListenerValue

    func next(_ value: ListenerValue)
    func complete()
    func error(_ error: Error)
}


final class AnyListener<T>: Listener
{
    typealias ListenerValue = T

    convenience init<L: Listener>(_ listener: L) where L.ListenerValue == ListenerValue {
        self.init(next: listener.next, complete: listener.complete, error: listener.error)
    }

    init(next: @escaping (ListenerValue) -> Void, complete: @escaping () -> Void, error: @escaping (Error) -> Void) {
        _next = next
        _error = error
        _complete = complete
    }

    func next(_ value: ListenerValue) { _next(value) }
    func complete() { _complete() }
    func error(_ error: Error) { _error(error) }

    private let _next: (T) -> Void
    private let _complete: () -> Void
    private let _error: (Error) -> Void
}

Above is a protocol with an associatedtype. The fact that it has the associatedtype means that we are unable to keep a property/outlet of the type directly. We must, somehow make a concrete version which is where the AnyType class comes in. We can wrap any Listener in an AnyListener of the appropriate type and, if the Listener conforms to the correct internal type, the compiler will allow it. As a bonus, there is a second constructor that accepts raw closures instead of a Listener. This allows you to convert any three closures which have the correct type signature into a Listener, no sub-classing necessary.

The above works as long as there are no constraints on what the associatedtype can be. In other words, none of the methods of the protocol are generic, but what do you do when there is a constraint of some sort? For example:

protocol Producer
{
    associatedtype ProducerValue

    func start<L: Listener>(for listener: L) where ProducerValue == L.ListenerValue
    func stop()
}

In the Producer protocol, you can see that the start method is generic on the Listener passed in and you can't pass in just any Listener object, it must be a Listener who's associatedtype is the same type as the Producer's associatedtype. How do we make the AnyProducer type here?

In other words, how do we implement this?

final class AnyProducer<T>: Producer
{
    typealias ProducerValue = T

    init<P: Producer>(_ producer: P) where P.ProducerValue == ProducerValue {
        fatalError("must implement")
    }

    func start<L: Listener>(for listener: L) where ProducerValue == L.ListenerValue {
        fatalError("must implement")
    }

    func stop() {
        fatalError("must implement")
    }
}

There are a very few articles on the internet that discuss a method of "boxing" the producer being passed into the constructor, but that doesn't give us the IMHO very useful init that allows us to pass in a couple of functions to create a Producer. I want to implement this Any type with methods, not a boxed producer.

For the stop method it's easy. After all, the stop method doesn't have any constraints:

final class AnyProducer<T>: Producer
{
    typealias ProducerValue = T

    init<P: Producer>(_ producer: P) where P.ProducerValue == ProducerValue {
        _stop = producer.stop
    }

    init(stop: @escaping () -> Void) {
        _stop = stop
    }

    func start<L: Listener>(for listener: L) where ProducerValue == L.ListenerValue {
        fatalError("must implement")
    }

    func stop() {
        _stop()
    }

    private let _stop: () -> Void
}

But implementing the start method, i.e. storing it for later use, proved to be more problematic... Passing in the start seems to be easy...

final class AnyProducer<T>: Producer
{
	typealias ProducerValue = T

	init<P: Producer>(_ producer: P) where P.ProducerValue == ProducerValue {
		_start = producer.start
		_stop = producer.stop
	}

	init<L: Listener>(start: @escaping (L) -> Void, stop: @escaping () -> Void) where ProducerValue == L.ListenerValue {
		_start = start
		_stop = stop
	}

	func start<L: Listener>(for listener: L) where ProducerValue == L.ListenerValue {
		fatalError("must implement")
	}

	func stop() {
		_stop()
	}

	private let _start: /* what should the type be here? */
	private let _stop: () -> Void
}

As mentioned in the comment, what type should _start be?

It can't be (Listener) -> Void because:

Protocol 'Listener' can only be used as a generic constraint because it has Self or associated type requirements

The input parameter type must be a concrete type... Well isn't that what we created AnyListener for? When we try that, we get an error on _start = start that says:

Cannot assign value of type '(L) -> Void' to type '(AnyListener<_>) -> Void'

Can we wrap the parameter inside the start method? How about we change the init method to take an AnyListener. That way, the types will match.

We end up with this class:

final class AnyProducer<T>: Producer
{
	typealias ProducerValue = T

	init<P: Producer>(_ producer: P) where P.ProducerValue == ProducerValue {
		_start = producer.start
		_stop = producer.stop
	}

	init(start: @escaping (AnyListener<T>) -> Void, stop: @escaping () -> Void) {
		_start = start
		_stop = stop
	}

	func start<L: Listener>(for listener: L) where ProducerValue == L.ListenerValue {
		_start(AnyListener<ProducerValue>(listener))
	}

	func stop() {
		_stop()
	}

	private let _start: (AnyListener<T>) -> Void
	private let _stop: () -> Void
}

I don't know if the above code is supposed to work or not, because the compiler crashes! (This is using Xcode 8 (8A218a), the final release of Xcode 8, not one of the betas.)

Call parameter type does not match function signature!
  %39 = bitcast i8* %38 to %swift.opaque*, !dbg !209
 %swift.type*  call void %48(%swift.opaque* noalias nocapture %0, %swift.opaque* %39, %swift.type* %8, i8** noalias nocapture %11, %swift.type* %13, i8** %49), !dbg !209
LLVM ERROR: Broken function found, compilation aborted!

After poking around for a while, I discovered that the problem is with the line _start = producer.start. The producer parameter's start method takes a Listener, not an AnyListener...

After some fruitless google searches and poking on it, I realized that I can wrap producer.start in a closure that does take an AnyListener type:

_start = { (l: AnyListener<ProducerValue>) -> Void in producer.start(for: l) }

The above is fully qualified, which isn't strictly necessary. Once we let type inference to its job:

_start = { producer.start(for: $0) }

And after that, we can refactor the two init methods so that one calls the other and end up with this class:

final class AnyProducer<T>: Producer
{
	typealias ProducerValue = T
	typealias ListenerType = AnyListener<T>

	convenience init<P: Producer>(_ producer: P) where P.ProducerValue == ProducerValue {
		self.init(start: { producer.start(for: $0) }, stop: producer.stop)
	}

	init(start: @escaping (ListenerType) -> Void, stop: @escaping () -> Void = { }) {
		_start = start
		_stop = stop
	}

	func start<L: Listener>(for listener: L) where ProducerValue == L.ListenerValue {
		_start(AnyListener(listener))
	}

	func stop() {
		_stop()
	}

	private let _start: (ListenerType) -> Void
	private let _stop: () -> Void
}

There we go. We have an AnyProducer that properly erases its type and we can create a Producer by simply constructing an AnyProducer with a couple of closures. The best of both worlds!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment