0
votes

I'm trying to design a strongly typed hierarchy of objects using protocols, but can't quite get it right.

For illustration, suppose that the concrete types that will end up adopting these protocols are the classes Country, State, and City.

Each node can have a parent (except the root object) and/or children (except the leaf objects): All State instances are children of the single Country instance and have it as parent, and they have City instances as children, etc.

So I start with these two protocols:

/// To be adopted by State and City
///
protocol Child: AnyObject {
    associatedtype ParentType: AnyObject 
    // (-> so that property `parent` can be weak)

    weak var parent: ParentType? { get }
} 

/// To be adopted by Country and State
///
protocol Parent: AnyObject {
    associatedtype ChildType: AnyObject
    // (-> for symmetry)

    var children: [ChildType] { get }
}

I have two separate protocols instead of one that groups all the above requirements, because I don't want to have to specify a "dummy" typealias ParentType for the root class Country (which is meaningless), nor a "dummy" typealias ChildType for the leaf class City.

Instead, I can separate the parent and child behaviours and just have the intermediate class State adopt both protocols.


Next, I want to make my clases initializable from a dictionary read from disk. For the child classes, I want to specify the parent at the moment of instantiation, so I came up with this scheme:

protocol UnarchivableChild: Child {
    init(dictionary: [String: Any], parent: ParentType?)
}

protocol UnarchivingParent: Parent {
    associatedtype ChildType: UnarchivableChild

    func readChildren(fromDictionaries dictionaries: [[String: Any]]) -> [ChildType]
}

As it stands, it looks like I could go one step further and add a default implementation of the method readChildren(fromDictionaries:), like this:

extension UnarchivingParent {
    func readChildren(fromDictionaries dictionaries: [[String: Any]]) -> [ChildType] {
        return dictionaries.flatMap({ dictionary in
            return ChildType(dictionary: dictionary, parent: self)
        })
    }
}

...because in this protocol, ChildType is constrained to UnarchivableChild, so it should support the initializer...? But I get:

Cannot invoke 'ChildType' with an argument list of type '(dictionary: ([String : Any]), parent: Self)'

(why the capitalized "Self"?)

I think I am missing something about how associated types work...

How can I code this default implementation?


Update: Apparently, passing self is the problem somehow. I modified the code to:

protocol Node: AnyObject {
}

protocol Parent: Node {
    associatedtype ChildNodeType: Node
    var children: [ChildNodeType] { get set }
}

protocol Child: Node {
    associatedtype ParentNodeType: Node
    weak var parent: ParentNodeType? { get set }
}

protocol UnarchivableChild: Child {
    init(dictionary: [String: Any]) // Removed parent parameter!
}

protocol UnarchivingParent: Parent {
    associatedtype ChildNodeType: UnarchivableChild
    func readChildren(fromDictionaries dictionaries: [[String: Any]]) -> [ChildNodeType]
}

extension UnarchivingParent {
    func readChildren(fromDictionaries dictionaries: [[String: Any]]) -> [ChildNodeType] {
        return dictionaries.flatMap({
            let child = ChildNodeType(dictionary: $0)
            // Assign parent here instead:
            child.parent = self // < ERROR HERE

            return child
        })
    }
} 

The error is:

Cannot assign value of type 'Self' to type '_?'

1

1 Answers

0
votes

After reading over The Swift Programmng Language: Generics (Section: "Extensions With a Generci Where Clause"), I found the appropriate syntax to achieve what I wanted. I settled for this code (type names are slighlty different form the ones in the original question):

protocol DictionaryInitializable {
    init(dictionary: [String: Any])
}

protocol ChildNode: AnyObject {
    associatedtype ParentType: AnyObject

    weak var parent: ParentType? { get set }
}

protocol ParentNode {
    associatedtype ChildType: AnyObject

    var children: [ChildType] { get set }
}

// This 'WHERE' clause fixes the issue:
extension ParentNode where ChildType: DictionaryInitializable,
ChildType: ChildNode, ChildType.ParentType == Self {

    func readChildren(from dictionaries: [[String: Any]]) -> [ChildType] {
        return dictionaries.map({
            return ChildType(dictionary: $0)
        })
    }
}

The first constraint of the where clause lets me call the initializer init(dictionary:), the second one guarantees the presence of the property parent, and the third one lets me assign self as its value.

Alternatively, I can change:

protocol ParentNode {
    associatedtype ChildType: AnyObject

to:

protocol ParentNode {
    associatedtype ChildType: ChildNode

...and skip the second constraint.