Swift: It's all in the protocols (Part III)


In the previous part of this series, I created a PartyPerson protocol and constructed classes around this. And here's the same protocol and classes with some minor tinkering:
protocol PartyPerson: class {
    var numberOfItemsOnYourPlate:Int {get}
    var name:String {get set}
    var isDrunk:Bool {get set}
    func tellUsABitAboutYourself() -> String
    func areYouDrunk() -> Bool
    func whatIsYourName() -> String

}

class YoungSocialite:PartyPerson {
    let food:[String]
    var numberOfItemsOnYourPlate:Int {
        return food.count
    }

    var isDrunk:Bool
    var name:String
    init (units:Int, name:String, food:[String]) {
        isDrunk = units > 3 ? true : false
        self.name = name
        self.food = food
    }
    func tellUsABitAboutYourself() -> String {
        return "OMG, I go out like twice every night. OMG, I know you. OMG. What a party. What a party. OMG."
    }
    func areYouDrunk() -> Bool {
        return isDrunk
    }
    
    func whatIsYourName() -> String {
        return name
    }
}

class ThirtySomething:PartyPerson {
    let varietiesOfCouscous:Int
    var numberOfItemsOnYourPlate:Int {
        return varietiesOfCouscous
    }
    var isDrunk:Bool
    var name:String
    init(units:Int, typesOfCouscous couscous:Int, name:String) {
        isDrunk = units > 9 ? true : false
        self.name = name
        varietiesOfCouscous = couscous
    }
    
    func tellUsABitAboutYourself() -> String {
        return "Should I have children? Or should I just keep doing this? You tell me. You look wise. Where's the toilet? What a lovely garden."
    }
    func areYouDrunk() -> Bool {
        return isDrunk
    }
    func whatIsYourName() -> String {
        return name
    }
}
I've added a name requirement and also restricted the protocol, so that it can only be applied to classes (and not structs or enums) by using the class keyword immediately after the semi-colon that follows the protocol name. I've also added some methods to return details about the person.

(Note: I learnt while testing the code contained in the latter part of this post that while a Playground would let me access class properties directly without complaint, when the app was built in the simulator a crash would occur. Hence the necessity of these added methods without which access to properties can become confused when using what Apple labels "collections of protocol types".)

Element protocols

The addition of the class restriction was just for fun, but the name property was added because I decided that I wanted to be a bit friendlier with my guests. I also decided that instead of calling the app itself the party, that I'd like to create an array of people and that array of people would represent a party:
let Dave = ThirtySomething(units: 8, typesOfCouscous: 5, name:"Dave")
let Brooklyn = YoungSocialite(units: 8, name: "Brooklyn", food: ["Manchego & Blackberries","Sun-dried Tomato and olive oil Bruschetta","Caprese Bites"])
        
let party:[PartyPerson] = [Dave,Brooklyn]
Notice how the use of a protocol enables us to group instances of different classes without resorting to AnyObject. The problem is now that we can't just write a generic method that accepts a protocol which the array type adopts, because the protocol won't know the methods that our PartyPerson protocol adoptees will respond to:
func mingle<P:SequenceType>(party:P) -> String {
    var chat = String()
    for person in party {
        chat += person.whatIsYourName() // error
        chat += ": \(person.tellUsABitAboutYourself())" // error
        chat += person.areYouDrunk() ? " (drunk)\n" : "\n" // error
    }
    return chat
} 
We need instead to let the function know what it will find inside the array (if we want to use any of our PartyPerson protocol methods).
func mingle<P:SequenceType where P.Generator.Element == PartyPerson>(party:P) -> String {
    var chat = String()
    for person in party {
        chat += person.whatIsYourName()
        chat += ": \(person.tellUsABitAboutYourself())"
        chat += person.areYouDrunk() ? " (drunk)\n" : "\n"
    }
    return chat
}
So what the heck have we done here? Well, we've let our mingle() function take any Type that adopts the SequenceType protocol as long as the elements within that Type have Elements that adopt the PartyPerson protocol.

(This is what P.Generator.Element is all about, every Type that adopts the SequenceType protocol has a generator and that generator enables us to iterate through the sequence and do things like use for-in. The elements, therefore, are each of the items in the sequence.)

Lessons learnt

In the documentation, Apple informs us that 'any protocol you create will become a fully-fledged type for use in your code ... [and] because it is a type, you can use a protocol in many places where other types are allowed, including: As a parameter type or return type in a function, method, or initialiser; As the type of a constant, variable, or property; As the type of items in an array, dictionary, or other container.'

The use of protocols to define types inside a collection is documented as well (under the heading 'Collections of Protocol Types'). However, it is clear from the experimentation that has gone into the writing of this article that, when using protocols to define types, an added awareness of the restrictions of accessing self, as well as accessing properties, need to be noted. (This is especially true when we reach the depths of combining collections of protocol types with generics.)

It must also be pointed out that the problems I experienced here might not appear in a Playground file but only arise in a Simulator or device build. And this might be both confusing and frustrating when debugging. But to be fair when we reach this deeply into a newly evolving programming language with so many innovations these things are to a certain extent to be expected.

For further discussion of the use of protocols to define types in collections, see this earlier post.

Technical note

For reference and for those currently debugging their own apps, the crash error that I received when I ran a version of the generic function that accessed properties directly was - malloc: *** error for object 0x7fc4c8509bb8: incorrect checksum for freed object - object was probably modified after being freed. *** set a breakpoint in malloc_error_break to debug.




Endorse on Coderwall

Comments