Skip to content

Instantly share code, notes, and snippets.

@davidkneely
Created July 7, 2017 10:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidkneely/f1614a1e61e2b8e4424f5cbdfdfae9f4 to your computer and use it in GitHub Desktop.
Save davidkneely/f1614a1e61e2b8e4424f5cbdfdfae9f4 to your computer and use it in GitHub Desktop.
Here is the playground I shared at the meetup tonight.
//: Playground - noun: a place where people can play
import UIKit
// source: The Swift Programming Language (Swift 4)
// https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Generics.html
/*
Does everyone know what a Type is?
What are some examples of Types?
Int, Double, String
Why do we need types? Compilation? Runtime?
Swift is Strongly-Typed.
Hereʻs a standard, nongeneric function called swapTwoInts(_:_:), which swaps two Int values
*/
// G E N E R I C S
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// swapTwoInts only works with Ints!
// letʻs update it to work with strings
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}
var someString = "3"
var anotherString = "107"
swapTwoStrings(&someString, &anotherString)
print("someString is now \(someString), and anotherString is now \(anotherString)")
// swapTwoStrings only works with Strings!
// letʻs update it to work with Doubles
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}
var someDouble = 3.0
var anotherDouble = 107.0
swapTwoDoubles(&someDouble, &anotherDouble)
print("someDouble is now \(someDouble), and anotherDouble is now \(anotherDouble)")
// notice that the body of each function is exaclty the same!
// What does DRY mean? Donʻt repeat yourself!
// Are we being DRY here?
// How can we make this swap function work on ANY KIND OF TYPE?
// Hereʻs our function using Generics
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
// Letʻs try out our generic function with different kinds of types (Int, String, Double)
// It works on all of them now!
// Ints
swapTwoValues(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Strings
swapTwoValues(&someString, &anotherString)
print("someString is now \(someString), and anotherString is now \(anotherString)")
// Doubles
swapTwoValues(&someDouble, &anotherDouble)
print("someDouble is now \(someDouble), and anotherDouble is now \(anotherDouble)")
// T is our way of putting a placeholder for Type
// It goes between <> right after the method name
// T Y P E P A R A M E T E R S
// <T> The type parameter is replaced with an actual type whenever the fucntion is called.
// N A M I N G T Y P E P A R A M E T E R S
// In most cases, type parameters have descriptive names, such as Key and Value in Dictionary<Key, Value> and Element in Array<Element>, which tells the reader about the relationship between the type parameter and the generic type or function itʻs used in.
// However, when there isnʻt a meaningful relationship between th itʻs traditional to name them using single letters such as T, U, and V.
// Always give type parameter upper camel case names (such as T and MyTypeParameter) to indicate that they are a placeholder for a type, not a value.
// G E N E R I C T Y P E S
// Custom classes, structures, and enumerations that can work with any type, in a similar way to Array and Dictionary.
// Letʻs build a stack
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop(_ item: Int) {
return items.removeLast()
}
}
var intStack = IntStack()
intStack.push(1)
intStack.pop()
// Now letʻs make it generic with generic types
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop(_ item: Element) {
return items.removeLast()
}
}
// for Ints
var stackOfInts = Stack<Int>()
stackOfInts.push(1)
stackOfInts.pop()
// for Strings
var stackOfStrings = Stack<String>()
stackOfStrings.push("Hello")
stackOfStrings.pop()
// for Doubles
var stackOfDoubles = Stack<Double>()
stackOfDoubles.push(1.1)
stackOfDoubles.pop()
// E X T E N D I N G A G E N E R I C T Y P E
// When you extend a generic type, you do not provide a type parameter list as part of the extensionʻs definiton. Instead, the type parameter list from the original type definition is available within the body of the extension, and the original type parameter names are used to refer to the type parameters from the original definiton.
extension Stack { // leverages struct Stack we created on line 131.
var topItem: Element? { // uses the Stacks existing type parameter name ʻElementʻ
return items.isEmpty ? nil : items[items.count - 1]
}
}
// Now this topItem can be used on any kind of Type in Stack
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem)")
}
// T Y P E C O N S T R A I N T S
// Allows you to be a bit more strict as to what kinds of Types are allowed.
// Hashable - Type must provide a way to make itself uniquely representable so it can be used to query for the value for the hashable key.
// All of Swiftʻs bsic types (String, Int, Double, and Bool) are hashsable by default.
// Equitable -
// T Y P E C O N S T R A I N T S Y N T A X
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// insert functionality here
}
// The above function says, T must be a subclass of SomeClass and U must conform to the SomeProtocol protocol.
// T Y P E C O N S T R A I N T S I N A C T I O N
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
// Use the function above to find a string value in an array of strings:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// Lets update this to be generic so we can find the index with Types in addition to String such as Int and Double
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind { // Swift compiler DOES NOT LIKE THIS LINE. Why?
return index
}
}
return nil
}
//GOTCHA: Not every Type in Swift can be compared with the equal to operator (==). If you create your own class or strcutrue to represent a complex data model, for example, then the meaning of "equal to" for that class or structure isnʻt somethign that Swift can guess for you. Because of this, it isnʻt possible to guarantee that this code will work for every possible type T, and an appropriate error is reported when you try to compile the code.
// How do we fix this? The Swift standard library defines a protocol called Equatable, which requires any conforming type to implement the equal to operator (==) and the not equal to operator (!=) to compare any two values of that type. All of Swiftʻs standard tpes automatically support the Equatable protocol.
// Letʻs update the findIndex function to force generic types to conform to equatable protocol.
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
// The above addition means: "any type T that conforms to the Equatable protocol"
// Now the function can be called wiht any type that is Equatable, such as Double or String.
let doubleIndex = findIndex(of: 9.3, in: [3.14, 0.1, 0.25])
let stringIndex = findIndex(of: "Andrea", in: ["Andrea", "Paul", "Ryan"])
// A S S O C I A T E D T Y P E S
// When defining a protocol, it is sometimes useful to declare one or more associated types as part of the protocolʻs definition. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isnʻt specified until the protocol is adopted. Associated types are specified with the associtedType keyword.
// A S S O C I A T E D T Y P E S I N A C T I O N
protocol Container {
associatedtype Item // this alias provides a way to refer to the type of the items in a Container, and to define a type for use with the append(_:) method and subscript, to ensure that the expected behavior of any Container is enforced.
mutating func append(_ item: Item) // must be possible to add new item
var count: Int { get } // must be possible to count items in container
subscript(i: Int) -> Item { get } // must be possible to retrieve each item with subscript that takes an Int index value
}
//NOTE: We have not defined what types the container can hold.
// Any type that conforms to the Container protocol must be able to specify the type of value it stores. Specifically, it must ensure that only items of the right type are added to the container, and it must be clear about the type of the items returned by its subscript.
// Hereʻs the nongeneric IntStack from before as it conforms to the Container protocol:
struct IntStack: Container {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop(_ item: Int) {
return items.removeLast()
}
// conformance to the Container protocol
// defines what kind of items we will be dealing with explicitly
// Turns the abstract type of Item into a concrete type of Int for this implementation of the Container protocol
// actually not necessary because IntStack only deals with Ints
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
// Now letʻs make the generic Stack conform to the Container protocol
struct Stack<Element>: Container {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop(_ item: Element) {
return items.removeLast()
}
// No need to typealias here because Swift infers that Element is the appropriate type to use as the Item for this particular container.
// conformance to Container protocol
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
// E X T E N D I N G A N E X I S T I N G T Y P E T O S P E C I F Y A N A S S O C I A T E D T Y P E
// Extend an existing type to add conformance to a protocol. Swiftʻs Array type already provides append(_:), count, and subscript with Int index to retrieve its elements. So you can do it like this:
extension Array: Container {}
// U S I N G T Y P E A N N O T A T I O N S T O C O N S T R A I N A N A S S O C I A T E D T Y P E
// Add a type annotation to an associated type in a protocol, to require that conforming types satisfy the constraints described by the type annotation.
// Hereʻs an updated Container that requires the items in the container to be equatable:
protocol Container {
// type annotation to constrain an associated type
// Now the Containerʻs type has to conform to the Equatable protocol
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
// G E N E R I C W H E R E C L A U S E S
// A generic where clause starts with the where keyword, followed by constraints for associated types or equality relationships between types and associated types. You write a generic where clause right before the opening curly brace of the type or fucntionʻs body.
func allItemsMatch<C1: Container, C2: Container>(_ someContiner: C1, _ anotherContiner: C2) -> Bool where C1.Item == C2.Item: Equatable {
if someContainer.count != anotherCountiner.count {
return false
}
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// all items match
return true
}
// Now letʻs test it out:
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("All items match")
} else {
print("Not all items match")
}
// E X T E N S I O N S W I T H A G E N E R I C W H E R E C L A U S E
// You can also use a generic where clause as part of an extension. The example below extends the generic Stack structure from teh previous examples to add an isTop(_:) method.
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool { // uses ==, so you have to make sure the types being passed in can be compared with == (Equatable type)
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
// Now letʻs use it.
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// Notice if you try to call isTop on a stack whose elements arenʻt equatable, youʻll get a compile-time error.
struct NotEquatable {}
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // Error
// You can use generic where clause with extensions to a protocol:
extention Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
// Letʻs prove that it can be used with any type that conforms to the Container protocol as long as the Container items are equatable.
if[9,9,9].startsWith(42) {
print("Starts with 42")
} else {
print("Starts with something else.")
}
// You can also write a generic where clause that requires Item to be a specific type:
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum/Double(count)
}
}
print(1260.0, 1200.0, 98.6, 37.0].average())
// You can include multiple where clauses, just make sure you separate them with a comma.
// A S S O C I A T E D T Y P E S W I T H A G E N E R I C W H E R E C L A U S E
// You can include a generic where clause on an associated type. Suppose you wanted to make a version of Container that includes an iterator, like what the Sequence protocol uses in the standard library. Hereʻs how:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}
// For a protocol that inherits from another protocol, you add a constraint to an inherited assocaited type by including the generic where clause in teh protocol declaration. For example, the following code declares a ComparableContainer protocol which requires Item to conform to Comparable:
protocol ComparableContainer: Container where Item: Comparable { }
// G E N E R I C S U B S C R I P T S
// Subscripts can be generic, and they can include generic where clauses. You write the placeholder type name inside angle brackets after subscript, and you write a generic where clause right before the opening curly brace of the subscriptʻs body:
extension Container {
subscript<Indices: Sequence>(indicies: Indices) -> [Item] where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment