The original article – Swift 4 Generics – was published on appcoda.com.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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:
1 2 3 4 5 6 7 |
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:
1 2 3 4 5 6 7 8 9 10 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func exists<T: Equatable>(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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,
1 |
func exists<T: Equatable>(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func find<T: Equatable>(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:
1 2 3 4 5 6 7 8 9 10 |
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! Why? Review this code and then we’ll talk about it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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) |
I’ve highlighted line 21 because it has this compiler error:
1 2 3 |
error: in argument type '[BasicPerson]', 'BasicPerson' does not conform to expected type 'Equatable' let isSamABasicPerson = exists(item: Sam, inArray: basicPersons) ^ |
The error shows up in the Xcode editor as this:
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 (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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 } } } |
I’ve highlighted lines 1 and 14-26 above to highlight Equatable
protocol conformance. 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:
1 2 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 |
I've highlighted line 16 above 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 -- and be sure to leave a comment if you have questions or feedback.
One thought on “Understanding Swift 4 generics and applying them in your code”
Comments are closed.