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
- Generic Stack
- Generic Queue
- Protocol with Generic
- Out of bound crash
- Generic Stack with Generic protocol
- Generic Queue with Generic protocol
- Summary
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
.
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.
One thought on “Swift Generic & Protocol”