Why protocols can only be used as generic constraints when they have Self or an associated type requirement.

Protocols with associated types are a powerful, if somewhat treacherous, feature of Swift. Sometimes it’s fair to say that the only winning move is not to play it.

As an example here is a recursive protocol of binary tree node with an associated value type. This declaration could be used to model generic binary trees. Your data type would have to implement this protocol as an extension and then you would be able to use some generic functions i.e. inserting your node in a binary tree.

protocol BinaryNode {
    associatedtype ValueType
    var value: ValueType { get }
    var left: BinaryNode? { get set }
    var right: BinaryNode? { get set }
}

But hold on you find yourself facing a difficult error when you want to compile this piece of code: “protocol can only be used as a generic constraint because it has Self or associated type requirements.”

To see why the compiler is complaining lets go ahead and see how we could use this protocol:

func insert(root: BinaryNode, _ next: BinaryNode) {
          ...    
}

Again the compiler will complain. The reason for this is that it is not clear that root and next would have the SAME value type. So someone could come up with the following definition:

class URLNode: BinaryNode {
      var value: URL(string:"www.swiftblog.kausch.li")
      var left: URL?
      var right: URL?
}


class StringNode: BinaryNode {
      var value: String = ""
      var left: StringNode?
      var right: StringNode?
}


But then the following code will create a tree of mixed nodes and break Swift’s type safety. And of course string and url are not comparable with each other and the code would break. This is why the compiler starts complaining when we start things messing up….

let urlNode = URLNode()
let stringNode = StringNode()
insert(root:urlNode,stringNode)   // BUUUM !!!

How is it possible to fix this? Well as the compiler says we need to use BinaryNode as a generic constraint and not as a type and it is the compiler that will figure out the associated types on it’s own. NICE!

protocol BinaryNode {

    associatedtype ValueType
    associatedtype BinaryNodeType

    var value: ValueType { get }
    var left: BinaryNodeType { get set }
    var right: BinaryNodeType { get set }
}


func insert<T: BinaryNode>(root: T, _ next: T) {
}

class StringNode: BinaryNode {
      var value: String
      var left: StringNode?
      var right: StringNode?
    
    init(_ value: String) {
        self.value = value
    }
}

let root = StringNode("hello")
let next = StringNode("world")

insert(root: root, next)


tomkausch

Leave a Reply

Your email address will not be published. Required fields are marked *