Feature image of Swift Generic introduction talk

Swift Generic introduction



The Generic realm is that kind of world, which we think we have some good understanding. And probably so. But things start to get messy when we initiate the implementation of so called Generic solution on our code base. The implementation doesn’t go so well as we used to see them on the example of books or blog-posts. Why? There are couple of reasons for that. Either we are not clear on the concepts of Generic. Or we still don’t have the understanding of when and how to implement the Generic solution on our code. So here on this blog post series we will talk about Generic. On this first blog post we will talk about Swift Generic introduction. After that on the second blog post we will have a more advance level implementation of Generic solution on our Swift code base.

As Apple already had some awesome details on Generic and its related concepts on different section on Apple doc on Generic. So going through those will definitely be a redundant talk.

However on this blog post we will touch the basic concepts of generic on Swift as an introduction talk through a very basic case study implementation. This time our case study will be Palindrome. Let us get started.

For a bit more advance level we can check out our Swift Generic & Protocol blog post.

Background

Our target is to build a simple playground project which can verify if an input is a Palindrome or not. On this case study we will add one by one requirement to make our solution generic. This should be the regular practice on implementing generic solution. On the evolution of the case study we will talk or share the reference of generic concepts if any needed.

On the playground project we will use the TDD approach to verify our tinny solution. We need not to fear on that, because it will be pretty simple and understandable. Big thanks to Sundell for his awesome snippets for Test on Playground. We will write a TestObserver to run our test on playground. Details code are available on GitHub.

And if we need some memory recall about snippets, then surely we can have a look on our snippets talk. Thats a small talk containing all the necessary things related with Snippets on Xcode. Video is also included to ease the different steps.

What is Palindrome

It is an interesting concept. But rather a simple one. Palindrome sequence is same on both forward and backward reading. We often say wow. This wow is a simple palindrome. As it is same on forward and backward read. Other examples are noon, madam, refer, mom etc. Guessing we can find a lot through simple google search on Palindrome, so moving on the case study implementation now.

The Test Observer snippet

To continuing our playground project we will use the following Snippets to run our test. Again thanks to Sundell.


class TestObserver: NSObject, XCTestObservation {
    func testCase(_ testCase: XCTestCase,
                  didFailWithDescription description: String,
                  inFile filePath: String?,
                  atLine lineNumber: Int) {
        assertionFailure(description, line: UInt(lineNumber))
    }
}

let testObserver = TestObserver()
XCTestObservationCenter.shared.addTestObserver(testObserver)
<#TestSuite#>.defaultTestSuite.run()

Detecting a String Palindrome

On the very start let us build a simple solution to detect if an input string is a Palindrome or not. We will write some test codes for verifying our solution.

Now what should be our first test case. Hmmm. Let say if a string is not a palindrome then we will say its not a palindrome, right? Simple. 🤓

As it goes on the TDD, let us first have a fail test case. So we will write:


func test_nonPalindrome_string_false() {
        let input = "nonPalindrome"
        XCTAssertFalse(input.isPalindrome, "\(input) is not be a Palindrome")
    }

As we can see the nonPalindrome is no way a palindrome. So we should have a false return on isPalindrome. But we have an error: Value of type 'String' has no member 'isPalindrome'. Hmm failed case. Let us define the minimal code to pass the test.


extension String{
    var isPalindrome: Bool {
        return false
    }
}

Now we have a isPalindrome which passes our test. But wait! Returning false not gonna work on future. Ya ya we know, but it’s the TDD way.

TDD approach

For the each test case repeat the following:

  • Write a fail test
  • Write minimal code to pass that test
  • Refactor the code

Then we will move to the next test case and follow the above approaches. And will continue on this way until all the test cases are passed. On some future time we will have details talk on TDD.

Continuing with string palindrome

Now for our next test case we will have a valid palindrome and for that the isPalindrome should return true.


func test_palindrome_string_true() {
        let input = "wow"
        XCTAssertTrue(input.isPalindrome, "\(input) is a Palindrome")
    }

Guess what, we have a fail test case. isPalindrome is returning false, where it should return true. Minimal code to fix that, right:

Replace the value of isPalindrome with return self == String(self.reversed()) where in previously it was returning false. So the isPalindrome on the String extension becomes as following:


extension String{
    var isPalindrome: Bool {
        return self == String(self.reversed())
    }
}

Re run the tests and everything pass. Now let us have some other tests to verify our solution.


func test_nonPalindrome_text_false() {
        let input = "This is a non-palindrome text"
        XCTAssertFalse(input.isPalindrome, "Should have return false for: \n \(input)")
    }
    
    func test_palindrome_text_true() {
        let input = "wow wow"
        XCTAssertTrue(input.isPalindrome, "Should have return true for: \(input)")
    }
    
    func test_nonPalindrome_textWithSpace_false() {
        let input = "wow wow "
        XCTAssertFalse(input.isPalindrome, "Should have return true for: \(input)")
    }
    
    func test_nonPalindrome_mixCase_false() {
        let input = "Wow wow"
        XCTAssertFalse(input.isPalindrome, "Should return false as there is a capital W on the\n \(input)")
    }
    
    func test_palindrome_mixCase_true() {
        let input = "Wow woW"
        XCTAssertTrue(input.isPalindrome, "Even case is mixed but this is a palindrome text:\n \(input)")
    }

Think we have cover for String type now. Let us move to other type such as Int.

How about a Palindrome of Int

Now heres come the new requirement. We need to support Int also. So what will be some example of Int palindrome? 121, 44, 12321 etc.

Our first test case is 100 which is not a palindrome.


func test_nonPalindrome_Int_false(){
        let input = 100
        XCTAssertFalse(input.isPalindrome, "\(input) is not a palindrome Int")
    }

Same way the test fails as isPalindrome is not defined for Int. Let us fix that.

Here we will go for a very simple implementation for palindrome in Int. We will just convert the Int to String and use the String type’s isPalindrome to verify.


extension Int{
    var isPalindrome: Bool{
        return String(self).isPalindrome
    }
}

Our primary goal of this talk is to build the generic environment rather than solving the palindrome problem. So let us stick with the solution and continue with it.

Now after having this test code we can see the test case100 is passing. Let us add a Palindrome Int then.


func test_palindrome_Int_true() {
        let input = 121
        XCTAssertTrue(input.isPalindrome, "\(input) is a palindrome Int")
    }

Ok this one also pass and finally we have a Int palindrome test.

How about some more test on Int.


    func test_longPalindrome_Int_true() {
        let input = 0000000000
        XCTAssertTrue(input.isPalindrome, "Long sequence on Zero should be a palindrome")
    }
    
    func test_nonPalindrome_negativeInt_false() {
        let input = -111
        XCTAssertFalse(input.isPalindrome, "\(input) should not be a palindrome")
    }

Hmm things are passing and we are good. Moving to Double palindrome.

Can we have Double Palindrome

Ok now how about Double palindrome. Palindrome on Double happens to be not only same sequence before and after the period ., but also the sequence should themself be palindrome like Int palindrome. So 131.131 is a palindrome. On the other hand 12.121 is not a palindrome. Even though the numbers without period/dot is a palindrome.

Time for some double palindrome test.


    func test_nonPalindrome_double_false() {
        let input = 12.01
        XCTAssertFalse(input.isPalindrome, "\(input) should not be a Palindrome Double")
    }
    
    func test_Palindrome_double_true() {
        let input = 12.21
        XCTAssertTrue(input.isPalindrome, "\(input) should be a Palindrome Double")
    }
    
    func test_Palindrome_double_true_aroundPeriod() {
        let input = 12.121
        XCTAssertFalse(input.isPalindrome, "Even though the 12121 is a palindrome, but \(input) must be palindrome around period")
    }

As we already guess for now the double isPalindrome is as following:


extension Double{
    var isPalindrome: Bool{
        return String(self).isPalindrome
    }
}

Converting into a more Generic solution for detecting Palindrome

Now we already have the big picture. So now let us have a introduction to a more Generic solution on our Swift code.

We gonna have a generic solution based on the way we just compare the String format of a type(Int, Double) with the reverse String format to validate the Palindrome sequence. For doing so we have to make sure that the type is confirming the LosslessStringConvertible protocol. Otherwise the conversion to String will fail on compile time. So our generic solution as follows:


func palindrome(input: T) -> Bool{
    let stringFormatted = String(input)
    return stringFormatted == String(stringFormatted.reversed())
}

As a result we can have the test as following:


func test_String() {
        let palindromeString = "wow wow"
        XCTAssertTrue(palindrome(input: palindromeString), "Should be a palindrome String")
        
        let nonPalindromeString = "nonPalindrome"
        XCTAssertFalse(palindrome(input: nonPalindromeString), "Should not be a palindrome string")
        
    }
    
    func test_Int() {
        let palindromeInt = 121
        XCTAssertTrue(palindrome(input: palindromeInt), "Should be a Int Palindrome")
        
        let nonPalindromeInt = 100
        XCTAssertFalse(palindrome(input: nonPalindromeInt), "Should not be a Palindrome Int")
    }
    
    func test_Double() {
        let palindromeDouble = 12.21
        XCTAssertTrue(palindrome(input: palindromeDouble), "Should be a Double Palindrome")
        
        let nonPalindromeDouble = 12.121
        XCTAssertFalse(palindrome(input: nonPalindromeDouble), "Should not be a Palindrome Double")
    }

Summarizing the generic evolution for the problem

Maybe the overall solution and the case study is pretty easy and strait forward. But this was meant to be an introductory post on Generic. So let us have some basic discussion aka easy-peasy talk with the generic solution. We will continue the discussion with some very basic question. Now we had the Generic method which is as follow:


func palindrome(input: T) -> Bool

What is the T?

So T defines a generic type, the type can be Int Double String or any other type. On run time the Generic type T will be replaced with the actual type.

Commute can be a similar example of generic type ,T , if we want to relate the generic concept with the real life. So when we say a 10 mins commute, the commute does not say anything about the commute type like walking biking or commuting through car bus. This just say a 10 mins commute. We implement the commute on run time or in real life when we start the commute through any of the commute type.

Now what if there is multiple Generic type. Convention is to use U or any other you like to use.

does T inherits from LosslessStringConvertible?

Second question does T inherits from LosslessStringConvertible? Well on Swift world we would use the term confirms to LosslessStringConvertible rather than the inherits. Because LosslessStringConvertible is a protocol. Ok but whats with the inheritance and protocol confirmation? Hmm let us have some shared links. Here you will get the inheritance talk. And here you will get the protocol confirmation talk. Both of these two are a series of continuous blog post where we discussed why composition over inheritance is better and how to achieve composition? Feel free to pay a visit on those blog post on your free time.

what does the T: LosslessStringConvertible mean?

Now in plain word T: LosslessStringConvertible means T can be any type but that type need to confirm LosslessStringConvertible protocol. Thats why the Float can not call the generic function as Float does not confirms the LosslessStringConvertible protocol.

Do the angle brackets < > has some value on generic?

It is a generic way of representing generic definition. Those two angle brackets tells the reader and the compiler that we have a generic type with its requirements.

Do we read input: T the same way as we do on other functions

Yes, that is what input: T mean. It says input is a T type of instance when it is compiled. And on run time the T is replaced with the actual type.

Why do we need the T to confirm LosslessStringConvertible

Because we are converting the Generic type to String when we are writing String(input), where input: T.

Best practice for implementing generic solution

Though this was a pretty easy talk on Generic introduction on Swift, but this can a good place to know when we should go for generic solution.

Let us consider another solution of this same problem through Swift extensibility feature. Let us extend LosslessStringConvertible to have the same functionality. So the final pice of code.


extension LosslessStringConvertible{
    var palindrome: Bool{
        let stringForm = String(self)
        return stringForm == String(stringForm.reversed())
    }
}

Now this version is very concise. So which one is the right approach?

If we use the approach of extending the LosslessStringConvertible protocol then all the types that confirms LosslessStringConvertible now have the palindrome var. Do our use case suggest that? If not the the generic function approach is the solution.

So let us finish this blog post by having some guidelines on best practice for implementing generic solution:

  • Generic solution is must when type safety is a major priority.
  • Generic solution is good for implementing algorithm.
  • When ever there is code duplication we can count generic on that case.
  • Most importantly we can avoid the ugly type casting.
  • And finally to restrict ourself from using Any.

Generic vs Any

On this Introduction of Swift Generic talk we have not cover a very important topic. The topic of when to choose Any over Generic or vice versa. Perhaps that needs a separated talk. On some future talk we will highlight the points for reducing the use of Any on our beloved code base.

End talk

It takes time and patient to set our mind for using Generic. This Swift Generic introduction blog post is the very first one of a series where we will try to achieve this goal. Let stay tune. The next one will not be so easy-peasy. The source code of this talk is shared on Github.

Reference

2 thoughts on “Swift Generic introduction

Leave a Reply

Notifications for mobidevtalk! Cool ;) :( not cool