Feature Image of Swift Generic & Protocol

Swift Generic & Protocol



In the Swift world, Generic is a very powerful tool the same way protocol is. In fact, the protocol is the core concept of Swift. The power of swift comes from its extensibility which is facilitated by protocol. When we are able to use these core concepts, protocol, with Generic our code becomes very agile. And we all know the benefit of Agile code when it comes to real software development. The target of this blog post is to initiate the talk about implementing a Swift generic solution through the protocol.

Here on this blog post, Swift Generic & Protocol, we will start with Generic Stack & Generic Queue. Then we will advance to a generic protocol-based solution for those Stack & Queue.

Background

The protocol is amazing stuff on Swift world. Here we can always have a visit to our protocol hub page where all the talks related to the protocol are listed. And on our last talk, we have some very basic about generic. Now it is time to mix them up.

Stack and Queue are two of the most common linear data structures. For this blog post, we will use the Stack and Queue to have some data operations. And obviously, we will use the Generic approach for a better solution. For more info on Stack and Queue.

We can move directly to the Out of bound crash section if we already have the basic idea about Stack and Queue.

Generic Stack

Let us have a generic Stack, generic in a scene that we will be able to use this stack for different types.

A quick note about the stack. Data operation on Stack is done through LIFO. Insertion is called push and deletion is called pop.


struct DS_Stack< Element >{
    private var items = [Element]()
    
    mutating func push(item: Element){
        items.append(item)
    }
    
    mutating func pop() -> Element{
        return items.removeLast()
    }
}

Here we use the Element keyword rather than the more common T for a generic type. Because Element has a better match for our current use case. Here is the basic talk over the generic syntax, if we need it.

Let us have Int and String Stack example.


var intStack = DS_Stack()
intStack.push(item: 200)
intStack.push(item: 2)

intStack.pop()

var stringStack = DS_Stack()
stringStack.push(item: "mobidevtalk")
stringStack.push(item: ".com")

stringStack.pop()

We will see 2 and .com was popped from the intStack and stringStack receptively.

Generic Queue

Now it is time for a generic Queue. Queue follows the FIFO approach for data operation. Insertion is called enqueue and deletion is called dequeue.


struct DS_Queue< Element >{
    private var items = [Element]()
    
    mutating func enqueue(item: Element){
        items.append(item)
    }
    
    mutating func dequeue() -> Element{
        return items.removeFirst()
    }
}

Example with Int and String as follows:


var intQueue = DS_Queue()
intQueue.enqueue(item: 200)
intQueue.enqueue(item: 2)

intQueue.dequeue()

var stringQueue = DS_Queue()
stringQueue.enqueue(item: "mobidevtalk")
stringQueue.enqueue(item: ".com")

stringQueue.dequeue()

So this time we will see 200 and mobidevtalk being removed as Queue follows the FIFO approach.

On the Stack and Queue we are actually mutating the struct because of the requirements of the current time. More on mutating the value type.

Out of bound crash

What if for both Stack and Queue we want to remove more elements than they have. So trying to removing the third element of a two-element Stack or Queue will generate a crash. Why? Because there is no element on that index. Let us fix that. We want to tell our users that they are trying to remove data from an empty data structure. Let us define the Error.


enum OperatingError: Error{
    case outOfBound
    case empty
}

If we are wondering why to use an enum for Error definition we can always visit the Error handling blog for some more details.

We will also have a description for the OperatingError. By confirming CustomStringConvertible we can have that.


extension OperatingError: CustomStringConvertible{
    var description: String{
        switch self {
        case .outOfBound:
            return "Oops! There is nothing to remove"
        case .empty:
            return "Empty Stack"
        }
    }
}

The CustomStringConvertible the protocol provides a customization point for describing. More on CustomStringConvertible.

Protocol with Generic

We have a basic understanding of Stack and Queue from the above sections. Now we should move to a more advanced data operating procedure. So the target is to generalize the data operation. And for that, we gonna use Protocol. But How?

By defining a DataOperator the protocol we will centralize the operations like inserting, deleting. Moreover, we will also add some status checks. Like the total count of elements. Also, we can add some description of the specific data structure.


protocol DataOperator: CustomStringConvertible{
    associatedtype Component
    
    mutating func insert(item: Component)
    mutating func remove() throws -> Component
    func numberOfComponent() throws -> Int
}

The CustomStringConvertible has a property called description. We want our DataOperator confirming types to provide a description of their Data structure.

Now let us talk about the associatedtype keyword. On Internet the associatedtype is a confusing word to understand when it comes to protocol and generic. We already talked about the associatedtype in Protocol, reach out to that post for more details. But for now, we will have the very basics of associatedtype.

associatedtype is a placeholder for type. What type? Any type. It cloud be Int String or some custom type. The practice of using the associatedtype is to define a placeholder type which will be replaced later on by some Concrete type on build time.

To make any protocol generic we need to make the operating type on that protocol generic. And that’s where associatedtype plays its role.

The insert and remove will mutate the underneath struct so the mutating keyword is used.

Generic Stack with Generic protocol

When we use the associatedtype with a protocol, that protocol becomes a more generic one. So Now let us use our generic protocol, DataOperator, in action. We gonna define Stack which will be generic itself and will also be able to use the generic protocol.


struct Stack< Element >: DataOperator{
    var description: String{
        "Stack is a linear Data structure. Follows the LIFO(Last In First Out) patterns. Only one tracker called Top."
    }
    
    private var items = [Element]()
    
    private mutating func push(item: Element){
        items.append(item)
    }
    
    private mutating func pop() -> Element{
        return items.removeLast()
    }
    
    typealias Component = Element
    
    mutating func insert(item: Element) {
        push(item: item)
    }
    
    mutating func remove() throws -> Element {
        if items.count == 0 {
            throw OperatingError.outOfBound
        }else{
            return pop()
        }
    }
    
    func numberOfComponent() throws -> Int {
        let count = items.count
        if count == 0 {
            throw OperatingError.empty
        }else{
            return count
        }
    }
}

We make the underneath data and its operation, items push pop, private. So those can not be accessed by others outside of the struct. At the same time, we are using the signature methods of DataOperator to make the data operation and status update.

On the typealias Component = Element we are defining the associated type as Element. So after this statement all the Component on the DataOperator in Stack will be replaced by Element. More interestingly when we will use the Int or String or any custom type of Stack the Element will be replaced by that concrete type.

Now we are throwing Error when the count is equal to zero both on remove and numberOfComponent. If we need some help on Error handling and its mechanism like the do try catch don’t tense. We have that cover on the Error Handling through try variance blog post.

Here comes the stack instance. This time we will use the Int based Stack. So the Element on the Stack and eventually the Component on the DataOperator will be replaced with the concrete type Int.


var stack = Stack< Int >()
stack.description

So we will have the stack description. Now it is time for the empty error state.


do {
    try stack.numberOfComponent()
} catch OperatingError.empty {
    OperatingError.empty.description
}

Empty Stack will be printed as we yet not inserted any of the elements. So let insert some.


stack.insert(item: 10)
stack.insert(item: 30)

Now, what about some removal. What if we want to remove more elements than the stack have.


for _ in 1..<4 {
    do {
        try stack.remove()
    } catch OperatingError.outOfBound {
        OperatingError.outOfBound.description
    }
}

Aha, we don't have the crash. We have the Oops! There is nothing to remove message been printed out. Savvy 🤓. Again if we need help on do try-catch.

Generic Queue with Generic protocol

So now it's time for some generic queue with the generic protocol. This will be nearly a copy on the stack other than the operation will be based on queue style.


struct Queue< Element >: DataOperator{
    var description: String{
        "Queue is a linear Data structure. Follows the FIFO(First In First Out) patterns. Uses two tracker. Font for insertion. Rear for deletion"
    }
    
    private var items = [Element]()
    
    private mutating func enqueue(item: Element){
        items.append(item)
    }
    
    private mutating func dequeue() -> Element{
        return items.removeFirst()
    }
    
    typealias Component = Element
    
    mutating func insert(item: Element) {
        enqueue(item: item)
    }
    
    mutating func remove() throws -> Element {
        if items.count == 0 {
            throw OperatingError.outOfBound
        }else{
            return dequeue()
        }
    }
    
    func numberOfComponent() throws -> Int {
        let count = items.count
        if count == 0 {
            throw OperatingError.empty
        }else{
            return count
        }
    }
}

Let us have a String queue and have the same operation as the stack.


var queue = Queue< String >()
queue.description

do {
    try queue.numberOfComponent()
} catch OperatingError.empty {
    OperatingError.empty.description
}

queue.insert(item: "mobidevtalk")
queue.insert(item: ".com")

for _ in 1..<4 {
    do {
        try queue.remove()
    } catch OperatingError.outOfBound {
        OperatingError.outOfBound.description
    }
}

Summary

Let us have a look at how the associatedtype got replaced by the more concrete type like Int. The following image is for Stack. The same will be for Queue.

Generic Protocol associatedtype
Generic with protocol type transforming

The above picture tells the story of Swift's generic solution through the protocol. All the source code used on this blog post is available on GitHub.

End Talk

Here on this blog post, Swift Generic & Protocol talk, We make the protocol generic when we use associatedtype. By defining the associatedtype on a protocol we remove the dependency from any specific/fixed type from that protocol. The protocol becomes more generic to embrace any type.

But we can't say we cover all the Generic & Protocol combination-related talk. This one was the tip of the iceberg. A long way to go. So stay tuned. Take care.

Reference

One thought on “Swift Generic & Protocol

Leave a Reply

Notifications for mobidevtalk! Cool ;) :( not cool