Search

How to Create Compile Time Polymorphism using Swift

Polymorphism is one of the base pillars of Object-oriented Programming (OOPs). Almost every developer who starts to code comes to realize this one way or the other. And then there are the varied formats of polymorphism, which may look daunting at some moments, with the technical jargon.

Polymorphism is many (poly) forms (morphs), which basically means one variable, one function, etc. can be in multiple formats. There are many ways to achieve polymorphism.

Today we are here to discuss one of those multiple formats, i.e. Compile Time Polymorphism or better known as Parametric Polymorphism.

Instead of defining multiple entities, we are trying here to make the logical approach in a more generic way; it will be accessed by as many entities as possible. This forms the basis of generic programming, where instead of creating multiple classes to define the logic, we aim to create a generic structure where the logic can be contained.


So how can we learn this? How can we implement this in Swift?

Let’s construct a logic before we move on to code: we want to have a class from where we will be able to identify what kind of language animals speak. For this purpose, we assume two animals – the Dog and the Cat. Cats say “Meow”, “Meow”, while Dogs say “Woof.”

How can we approach this? We know that there will be a class to accomplish it. Let’s call it Speakers. In Swift, there’s a feature that is often ignored; this is called Generics.

As the official doc states :

“Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.”

Which means we can employ Swift generics to construct the aforementioned concept here.

First, let’s define two classes as follows.

class Dog {
}

class Cat {
}

And there will be a third class called ‘I’, where the logic to call the corresponding animals will have to be written. (The idea is I call a dog and the response will be printed out)

class I {
 }

Now we need to add the speak function to each of the classes.

We will create a protocol here called Response, which will have this speak function. Both Dog and Cat will have to conform to this Response protocol.

protocol Response {
      func speak () 
}

But as we know, a cat will respond to “meow” “meow” which clearly is a collection while a dog will respond only to “woof”, which is a string.

So we will have to make the protocol return a value whose return type may vary based on the class itself.

Let’s rewrite Response Protocol:

protocol Response {
    associatedtype Voice
    func speak () -> Voice
}

Good. This means any class that conforms to Response Protocol will have a voice to speak.

Let’s add this to our classes Dog and Cat and rewrite it:

class Dog : Response {
    
    typealias Voice = String
    
    func speak() -> String {
        return "woof"
    }
}
class Cat : Response {
    
    typealias Voice = [String]
    
    func speak() -> [String] {
        return ["meow","meow"]
    }
} 

When a class conforms to Response Protocol, it will have to define what its voice value type is. For class Dog, it’s a String, for class Cat it’s an array of String or [Strings].

Now we have to write the Speakers class logic

The idea for call function in ‘I’ is like this → ‘I’ will have a function named call, where the i/p will be an object of the class that conforms to Response Protocol.

class I {
    
    class func call<T: Response>(_ speaker: T) -> T.Voice {
        return speaker.speak()
    }
}

Let’s break it down:

The function call() takes in an i/p which is referred as speaker. This speaker is of T type, where T is referring to Response protocol. The return type is the Voice of the speaker.

Instead of using an actual type for the return value (such as String , Array for this case), we are using a placeholder type (T in this instance), where it represents any class that conforms to Response protocol. Based on what the speaker will be, the return value will vary, i.e. it will be defined in compile time. The call function can have any form of object that conforms to Response and based on that object its response will also be of different types. This is the Parametric Polymorphism.

Let’s go through the entire logic:

protocol Response {
    associatedtype Voice
    func speak () -> Voice
}

class Dog : Response {
    
    typealias Voice = String
    
    func speak() -> String {
        return "woof"
    }
}

class Cat : Response {
    
    typealias Voice = [String]
    
    func speak() -> [String] {
        return ["meow","meow"]
    }
}

class I {
    
    class func call<T: Response>(_ speaker: Response) -> T.Voice {
        return speaker.speak()
    }
}

To test, let’s add some more codes to your playground:

let response = I.call(Dog())

print("A dog \(response)")

let catResp = I.call(Cat())

print("A cat \(catResp)")

Output:

A dog woof
A cat ["meow", "meow"]

For class Dog, the Voice is a String type, it returns a string, for class Cat, it’s an array.

Hope this makes sense. As always, if you have any question or feedback, please feel free to comment below.

Comments

comments

Awards:
  • Economic Times India's Growth Champion 2020
Accreditation & Partnership:
  • CII