Swift · · 9 min read

Swift Generics: How to Apply Them in Your Code and iOS Apps

Swift Generics: How to Apply Them in Your Code and iOS Apps

Question 1: Can I write one Swift function that can find the index/position of any specific instance of any type stored in any array that stores objects of that type?

Question 2: Can I write one Swift function that can determine if any specific instance of any type exists in any array that stores objects of that type?

When I say “any type,” I mean including custom types (like classes) that I define myself. NOTE: Yes, I know that I could use the Swift Array type’s built-in functions, index and contains, but I’ll be using simple example code today to illustrate some points about Swift generics.

In general, I’ll be covering generic programming, defined as:

… a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters. This approach, pioneered by ML in 1973, permits writing common functions or types that differ only in the set of types on which they operate when used, thus reducing duplication.

Specifically, from the “Generics” topic in Apple’s Swift documentation:

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.

Generics are one of the most powerful features of Swift, and much of the Swift standard library is built with generic code. … For example, Swift’s Array and Dictionary types are both generic collections. You can create an array that holds Int values, or an array that holds String values, or indeed an array for any other type that can be created in Swift. Similarly, you can create a dictionary to store values of any specified type, and there are no limitations on what that type can be. …

I’ve always been a strong proponent of code reuse, simplicity, and maintainability, and Swift’s generics, when used appropriately, go a long way in terms of helping me achieve these techniques I advocate. So the answer to Question 1 and Question 2 above is “yes.”

Living in a specific programming world

Let’s write a Swift function to tell us if a specific string exists in an array of strings:

func existsManual(item:String, inArray:[String]) -> Bool
{
    var index:Int = 0
    var found = false
    
    while (index < inArray.count && found == false)
    {
        if item == inArray[index]
        {
            found = true
        }
        else
        {
            index = index + 1
        }
    }
    
    if found
    {
        return true
    }
    else
    {
        return false
    }
}

Let's test the function:

let strings = ["Ishmael", "Jacob", "Ezekiel"]

let nameExistsInArray = existsManual(item: "Ishmael", inArray: strings)
// returns true

let nameExistsInArray1 = existsManual(item: "Bubba", inArray: strings)
// returns false

After creating the "existsManual" function for searching String arrays, suppose I decided I wanted similar functions for searching Integer, Float, and Double arrays -- even for searching arrays of custom classes that I write? I'd end up spending precious time writing a lot of functions that all do the same thing. I'd have more code to support. Suppose I discovered a new/faster searching algorithm? Suppose I found a bug in my search algorithm? I'd have to change all versions of my search functions. Here's the duplication hell in which I'd find myself:

func existsManual(item:String, inArray:[String]) -> Bool
...
func existsManual(item:Int, inArray:[Int]) -> Bool
...
func existsManual(item:Float, inArray:[Float]) -> Bool
...
func existsManual(item:Double, inArray:[Double]) -> Bool
...
// "Person" is a custom class we'll create
func existsManual(item:Person, inArray:[Person]) -> Bool

The problem

In a world in which we're so tied to types, where we'd have to create a new function for every typed array we want to search, we'd end up with lots of technical debt. Because of the incredible complexity of modern software, developers like you and I need to use the best practices, the best technologies, the best methodologies, and use our own neurons to the fullest to control this chaos. It was estimated that Windows 7 contained approximately 40 million lines of code while macOS 10.4 (Tiger) contained about 85 million lines. Estimating the number of possible behaviors which such systems can exhibit is computationally impossible.

Generics to the rescue

(Remember again that for the purpose of learning about generics, we're still pretending that the Swift Array type's built-in functions, index and contains, don't exist.)

Let's limit ourselves to the scope of trying to write one Swift function to tell us if a specific instance of one of Swift's standard types exists in an array of the same Swift standard type, respectively, e.g., like String, Integer, Float, and Double. How?

Let's turn to Swift generics, specifically, generic functions, type parameters, type constraints, and the Equatable protocol. Without defining any of these terms yet, I'll write some code and let you think about what you see:

func exists(item:T, inArray:[T]) -> Bool
{
    var index:Int = 0
    var found = false
    
    while (index < inArray.count && found == false)
    {
        if item == inArray[index]
        {
            found = true
        }
        else
        {
            index = index + 1
        }
    }
    
    if found
    {
        return true
    }
    else
    {
        return false
    }
}

Let's test my new generic function:

let myFriends:[String] = ["John", "Dave", "Jim"]

let isOneOfMyFriends = exists(item: "Dave", inArray: myFriends)
// returns true

let isOneOfMyFriends1 = exists(item: "Laura", inArray: myFriends)
// returns false

let myNumbers:[Int] = [1,2,3,4,5,6]

let isOneOfMyNumbers = exists(item: 3, inArray: myNumbers)
// returns true

let isOneOfMyNumbers1 = exists(item: 0, inArray: myNumbers)
// returns false

let myNumbersFloat:[Float] = [1.0,2.0,3.0,4.0,5.0,6.0,]

let isOneOfMyFloatNumbers = exists(item: 3.0000, inArray: myNumbersFloat)
// returns true

My new "exists" function is a generic function, one that "can work with any type." Furthermore, when we look at the function's signature,

func exists(item:T, inArray:[T]) -> Bool

we see that my "function uses a placeholder type name (called T, in this case) instead of an actual type name (such as Int, String, or Double). The placeholder type name doesn't say anything about what T must be, but it does say that both [item] and [inArray] must be of the same type T, whatever T represents. The actual type to use in place of T is determined each time the [exists(_:_:)] function is called."

The "exists" function's placeholder type T is what's called a type parameter, which:

"specify and name a placeholder type, and are written immediately after the function's name, between a pair of matching angle brackets (such as <T>).

Once you specify a type parameter, you can use it to define the type of a function's parameters (such as the [item] and [inArray] parameters of the [exists(_:_:)] function), or as the function's return type, or as a type annotation within the body of the function. In each case, the type parameter is replaced with an actual type whenever the function is called."

To reinforce what we've learned so far, here's one Swift function that can find the index/position of any specific instance of any type stored in any array that stores objects of that type:

func find(item:T, inArray:[T]) -> Int?
{
    var index:Int = 0
    var found = false
    
    while (index < inArray.count && found == false)
    {
        if item == inArray[index]
        {
            found = true
        }
        else
        {
            index = index + 1
        }
    }
    
    if found
    {
        return index
    }
    else
    {
        return nil
    }
}

Let's test it:

let myFriends:[String] = ["John", "Dave", "Jim", "Arthur", "Lancelot"]

let findIndexOfFriend = find(item: "John", inArray: myFriends)
// returns 0

let findIndexOfFriend1 = find(item: "Arthur", inArray: myFriends)
// returns 3

let findIndexOfFriend2 = find(item: "Guinevere", inArray: myFriends)
// returns nil

About Equatable

What's this <T: Equatable> annotation on my "exists" function? It's called a type constraint, and it specifies "that a type parameter must inherit from a specific class, or conform to a particular protocol or protocol composition." I'm specifying that my "exists" function parameters, item:T and inArray:[T], must be of type T, and type T must conform to the Equatable protocol. Why?

All the Swift built-in types have been constructed to support the Equatable protocol. From the Apple docs: "Types that conform to the Equatable protocol can be compared for equality using the equal-to operator (==) or inequality using the not-equal-to operator (!=)." That's why my generic function "exists" works with Swift types like String, Integer, Float, and Double. All these types define the == and != operators.

Custom types and generics

Suppose I create a new class called "BasicPerson" and define it as shown below. Can I use my "exists" function to find out if an instance of "BasicPerson" occurs in an array of that type? NO! And why? Review this code and then we'll talk about it:

class BasicPerson
{
    var name:String
    var weight:Int
    var sex:String
    
    init(weight:Int, name:String, sex:String)
    {
        self.name = name
        self.weight = weight
        self.sex = sex
    }
}

let Jim = BasicPerson(weight: 180, name: "Jim Patterson", sex: "M")
let Sam = BasicPerson(weight: 120, name: "Sam Patterson", sex: "F")
let Sara = BasicPerson(weight: 115, name: "Sara Lewis", sex: "F")

let basicPersons = [Jim, Sam, Sara]

let isSamABasicPerson = exists(item: Sam, inArray: basicPersons)

Look at the last line because it has this compiler error:

error: in argument type '[BasicPerson]', 'BasicPerson' does not conform to expected type 'Equatable'
let isSamABasicPerson = exists(item: Sam, inArray: basicPersons)
                                                   ^

Swift generics

As if that's not bad enough, you can't use the Swift Array type's built-in functions, index and contains on arrays of the "BasicPerson" type. (You'd have to define a closure each time you wanted to use those two methods and blah, blah, blah... I'm not even going there.)

So again, why?

Because the "BasicPerson" class doesn't conform to the Equatable protocol (this is a hint, hint, 😉 for reading the rest of this article).

Conforming to Equatable

In order to allow my "BasicPerson" class to work with my "exists" and "find" generic functions, all's I need to do is:

  • Mark the class as adopting the Equatable protocol; and,
  • Overload the == operator for class instances.

Note that "The standard library provides an implementation for the not-equal-to operator (!=) for any Equatable type, which calls the custom == function and negates its result."

If you're not familiar with operator overloading, I suggest you read up on the topic at these links here and here. Trust me, you want to understand operator overloading.

Note: I'm renaming the "BasicPerson" class to "Person" so they can co-exist in the same Swift playground. From here onwards, I'll be referring to the "Person" class.

I'll implement the == operator so that it compares the "name," "weight," and "sex" properties of one instance of the "Person" class with another. If two "Person" class instances have the same three properties, they're equal. If any of the properties differ, they're not equal (!=). Here's how my "Person" class adopts the Equatable protocol:

class Person : Equatable
{
    var name:String
    var weight:Int
    var sex:String
    
    init(weight:Int, name:String, sex:String)
    {
        self.name = name
        self.weight = weight
        self.sex = sex
    }
    
    static func == (lhs: Person, rhs: Person) -> Bool
    {
        if lhs.weight == rhs.weight &&
            lhs.name == rhs.name &&
            lhs.sex == rhs.sex
        {
            return true
        }
        else
        {
            return false
        }
    }
}

Note the == overload above, which makes "Person" conform to the Equatable protocol. Note the arguments lhs and rhs in the == overload. It's common, when operator overloading, to name the arguments to which the operator is being applied in regards to their physical positions in code, like so:

lhs == rhs
left-hand side == right-hand side

Will it work?!?!?

If you follow my directions, you'll be able to create generic functions like my "exists" and "find" for use with any new types you create, like classes or structs. You'll also be able to use Swift's built-in functions, like index and contains, with Array type collections of your custom Equatable protocol-conforming classes and structs. It does work:

let Joe = Person(weight: 180, name: "Joe Patterson", sex: "M")
let Pam = Person(weight: 120, name: "Pam Patterson", sex: "F")
let Sue = Person(weight: 115, name: "Sue Lewis", sex: "F")
let Jeb = Person(weight: 180, name: "Jeb Patterson", sex: "M")
let Bob = Person(weight: 200, name: "Bob Smith", sex: "M")

let myPeople:Array = [Joe, Pam, Sue, Jeb]

let indexOfOneOfMyPeople = find(item: Jeb, inArray: myPeople)
// returns 3 from custom generic function

let indexOfOneOfMyPeople1 = myPeople.index(of: Jeb)
// returns 3 from built-in Swift member function

let isSueOneOfMyPeople = exists(item: Sue, inArray: myPeople)
// returns true from custom generic function

let isSueOneOfMyPeople1 = myPeople.contains(Sue)
// returns true from built-in Swift member function

let indexOfBob = find(item: Bob, inArray: myPeople)
// returns nil from custom generic function

let indexOfBob1 = myPeople.index(of: Bob)
// returns nil from built-in Swift member function

let isBobOneOfMyPeople1 = exists(item: Bob, inArray: myPeople)
// returns false from custom generic function

let isBobOneOfMyPeople2 = myPeople.contains(Bob)
// returns false from built-in Swift member function

if Joe == Pam
{
    print("they're equal")
}
else
{
    print("they're not equal")
}
// returns "they're not equal"

Further reading

Apple notes the benefits of the Equatable protocol -- and more:

Adding Equatable conformance to your custom types means that you can use more convenient APIs when searching for particular instances in a collection. Equatable is also the base protocol for the Hashable and Comparable protocols, which allow more uses of your custom type, such as constructing sets or sorting the elements of a collection.

For example, if you adopt the Comparable protocol, you can overload and make use of the <, >, <=, and >= operators. Pretty cool.

Beware

Think about the "Person" class and a situation where we have instances like these:

let Joe = Person(weight: 180, name: "Joe Patterson", sex: "M")
let Pam = Person(weight: 120, name: "Pam Patterson", sex: "F")
let Sue = Person(weight: 115, name: "Sue Lewis", sex: "F")
let Jeb = Person(weight: 180, name: "Jeb Patterson", sex: "M")
let Bob = Person(weight: 200, name: "Bob Smith", sex: "M")
let Jan = Person(weight: 115, name: "Sue Lewis", sex: "F")

if Jan == Sue
{
    print("they're equal")
}
else
{
    print("they're not equal")
}
// returns "they're equal" for 2 different objects

Look at the last line because "Person" objects "Jan" and "Sue" are technically equal, even though they're two different class instances. Your software is only as good as you design it. In database terminology, you'd need a "primary key" in a collection of "Person" classes -- maybe add a variable for a GUID to the class design, or a social security number, or some other value you know would be guaranteed to be unique in a collection (array) of "Person" class instances. Or, you could use ===.

Enjoy!

Read next