Saturday, June 13, 2015

A Scala Developer's Perspective on Swift Protocols

Alternate title: error: protocol 'Foo' can only be used as a generic constraint because it has Self or associated type requirements

Swift is cool. It's rocking that functional paradigm without giving up the OO strengths that make it possible to comprehensibly compose massively complex applications. It reminds me of Scala, which straddles that same gap comfortably and to great effect. However... what the heck is going on with typealiases in protocols?

I'd like to give a motivating example. Here is a pretty canonical example of the Observer Pattern written in Scala:
    trait Observer[T] {
      def notifyMe(param: T)
    }

    var notified = 0

    val intObservers: List[Observer[Int]] = List(
      new Observer[Int] {
        def notifyMe(param: Int) {
          notified = 1
        }
      }
    )

    assert(notified == 0)
    intObservers.foreach(_.notifyMe(1))
    assert(notified == 1)
Pretty straightforward. We have a generic observer trait that we specialize to a registry that accepts ints, then we notify all observers in the registry with an int param. Now let's have a look at how we might (naively) implement such a pattern in Swift:
    protocol Observer {
        typealias T
        func notifyMe(param: T)
    }

    var notified = 0

    // super annoying, no anonymous classes in Swift!
    class ObserverImpl: Observer {
        typealias T = Int
        func notifyMe(param: T) {
            notified = param
        }
    }

    var intObservers: [Observer<Int>] = [AnyObserver(ObserverImpl())] 

    assert(notified == 0)
    intObservers.forEach { $0.notifyMe(1) } 
    assert(notified == 1)  
Again pretty straightforward but this time on compile we get the following errors:
Cannot specialize non-generic type 'Observer'
Member 'notifyMe' cannot be used on a value of protocol type 'Observer'; use a generic constraint instead
What? I'm really not allowed to constrain my generic protocol to a specialized type that I can concretely operate on? Yep. Them's the breaks -- protocols really are not interfaces, and once you add typealiases to them you lose all ability to refer to them concretely in your program. So that sucks, but fortunately there's a semi-reasonable way around this! Apple themselves apply this pattern all over the place in the standard Swift libraries because, it turns out, generic programming is really useful - and a huge part of that is the need to refer to type-constrained generic interfaces. The solution is to wrap our generic protocol in a concrete class. The new struct AnyObserver is providing the compiler with additional type information that it can't guarantee from the protocol at compile-time.
    protocol Observer {
        typealias T
        func notifyMe(param: T)
    }

    struct AnyObserver<T>: Observer {
        private let _notifyMe: (T) -> Void
        
        init<O: Observer where T == O.T>(_ observer: O) {
            _notifyMe = observer.notifyMe
        }
        
        func notifyMe(param: T) {
            _notifyMe(param)
        }
    }

    var notified = 0

    class ObserverImpl: Observer {
        typealias T = Int
        func notifyMe(param: T) {
            notified = param
        }
    }

    var intObservers: [AnyObserver<Int>] = [AnyObserver(ObserverImpl())]

    assert(notified == 0)
    intObservers.forEach { $0.notifyMe(1) }
    assert(notified == 1)
So for a bit of extra code we get to have our cake and eat it too. In this contrived example that seems like a pretty good deal, but if you imagine the complexity of generic interfaces you can come up with in, e.g., a large enterprise app (or heck Facebook or something), you might think twice about having to repeat your interfaces everywhere you have an Any* wrapper. C'est la Swift.