Code Challenge: Swift Generics and Type Conformance

Generics & Swift

The ability to create generic models in Swift provides a powerful, yet flexible model for code reuse. Generics may not come up as the main discussion in a technical interview, but if used correctly, will certainly showcase your ability to create a flexible design. Swift generics can be used to extend straightforward or complex code and is embedded in many aspects of the language. To understand how this works, let’s review the challenge of comparing Apples and Oranges:

/* 
challenge: Identify and correct the syntax and/or design issues with the following code. Assume basic type definitions for Apple and Orange are provided.
*/

func isBetter<T>(lhs: T, rhs: T) -> T {    
    if lhs > rhs {
        return lhs
    }    
    return rhs
}

//hint - see parameters..
let results = isBetter(lhs: Apple, rhs: Orange)

To review, the main goal of generics is to extend a function (or object) to support different types. As shown, isBetter accepts two generic parameters and returns the larger value. We can also conclude our main objective is to compare the parameters and determine the better result. Our challenge is writing code to evaluate items that potentially differ in behavior and design.

Conforming Types

Let’s proceed to make a few changes. First, we should consider the idea of comparing Apples to Oranges. While it would be possible to create an algorithm to determine which fruit was preferred in a given dataset, let’s first focus on comparing two instances of the same type using a taste score:

struct Orange: Comparable {    
    var name: String
    var taste: Int
    ...
}

As part of our implementation, notice how Orange now supports the popular (Swift SDK) Comparable protocol. This feature provides a set of consistent rules needed for making (logical) comparisons. For Orange to fully conform to Comparable, let’s add some additional behavior:

struct Orange: Comparable {    
    var name: String
    var taste: Int
    
    static func <(lhs: Orange, rhs: Orange) -> Bool {
        return lhs.taste < rhs.taste
    }
}

With these changes in place let’s review our solution. Notice that isBetter also accepts generic parameters that support the Comparable type constraint:

func isBetter<T: Comparable>(lhs: T, rhs: T) -> T? {    
    if lhs > rhs {
        return lhs
    }
        
    else if lhs < rhs {
        return rhs
    }    
    return nil
}

...

let a = Orange(name: "Tangerine", taste: 9)
let b = Orange(name: "Clementine", taste: 5)

//works as expected
let result = isBetter(lhs: a, rhs: b)

Making Things Edible

Our code works but could be refined even further. Instead of having a model that compares only Oranges, we can utilize Swift generics to support all kinds of fruit. The first step involves writing a new custom Edible protocol. Based on our previous taste score, this new type will also inherit from Comparable:

protocol Edible: Comparable {
    var taste: Int 
}

With this change in place, we can now refactor the Orange type to support any type of Fruit:

func isBetter<T: Edible>(lhs: T, rhs: T) -> T? {
  ...
}
 
struct Fruit: Edible {
   var taste: Int
   var hasSeeds: Bool = true  
   ...
}

let a = Fruit(name: "Apple", taste: 9)
let b = Fruit(name: "Orange", taste: 5)

let result = isBetter(lhs: a, rhs: b)