Skip to content

Instantly share code, notes, and snippets.

@ws909
Last active December 16, 2019 21:14
Show Gist options
  • Save ws909/453f43bb1f5a2e1528279c3ff572466e to your computer and use it in GitHub Desktop.
Save ws909/453f43bb1f5a2e1528279c3ff572466e to your computer and use it in GitHub Desktop.
An example showing how key paths can be useful when programming in Swift

Swift KeyPath example

Ever wondered how key paths can be useful in programming? Let's try to sort arrays, and find items in them, based on properties of the elements, in this case a struct. To do so, we can create a set of generic functions and methods, and use a KeyPath as one of the arguments, specifying the desired property of the elements in the array to process.

First, a filtering function is written. It's a generic function that simply returns a new array containing the elements from a given one, that meet the required condition. It drastically shortens the code needed to write for each case: let s2 = searchFor(property: \String.count, ==, 2, in: ["a", "ab", "abc", "cd"]) // s2 = ["ab", "cd"]

@inline(__always)
func searchFor<Root, Value>(property: KeyPath<Root, Value>, _ condition: (Value, Value) -> Bool, _ value: Value, in array: [Root]) -> [Root] {
    
    var results = [Root]()
    
    for container in array {
        let propertyValue = container[keyPath: property]
        if !condition(propertyValue, value) { continue }
        
        results.append(container)
    }
    
    return results
}

Let's add the the sorting functions as well, taking advantage of Swift's Standard Library's sorting algorithms. We can also include the searchFor function as a method.

extension Array {
    
    mutating func sort<T>(by property: KeyPath<Element, T>, _ validator: (T, T) throws -> Bool) rethrows {
        try self.sort { try validator($0[keyPath: property], $1[keyPath: property]) }
    }
    
    func sorted<T>(by property: KeyPath<Element, T>, _ validator: (T, T) throws -> Bool) rethrows -> [Element] {
        return try self.sorted { try validator($0[keyPath: property], $1[keyPath: property]) }
    }
    
    mutating func sort<T: Comparable>(by property: KeyPath<Element, T>) {
        self.sort { $0[keyPath: property] < $1[keyPath: property] }
    }
    
    func sorted<T: Comparable>(by property: KeyPath<Element, T>) -> [Element] {
        return self.sorted { $0[keyPath: property] < $1[keyPath: property] }
    }
    
    func searchFor<Value>(property: KeyPath<Element, Value>, _ condition: (Value, Value) -> Bool, _ value: Value) -> [Element] {
        // The compiler forces the use of the module name, even tho it recognizes the correct function.
        return My_Module.searchFor(property: property, condition, value, in: self)
    }
}

To make the output more readable, this extension can be added. It's not possible to override the description property of Array, so we're required to create our own.

extension Array {
    
    var depiction: String {
        var string = "["
        
        for element in self {
            string += "\n    \(element),"
        }
        
        return (string - 1) + "\n]"
    }
}

/// Returns a new `String` having r number of characters removed from the given string.
func -(l: String, r: Int) -> String {
    return String(l[..<l.index(l.endIndex, offsetBy: -r)])
}

Test

Now that the code we need is implemented, a test can be written:

struct Book {
    let pages: Int
    let year: Int
}
let books = [
    Book(pages: 365, year: 2002),
    Book(pages: 253, year: 2016),
    Book(pages: 100, year: 2017),
    Book(pages: 438, year: 2018),
    Book(pages:  92, year: 1993),
    Book(pages: 369, year: 1954),
    Book(pages: 312, year: 1988),
    Book(pages: 415, year: 1988),
    Book(pages: 284, year: 1976),
    Book(pages: 284, year: 2010)
]
    
let new   = searchFor(property: \Book.year,  >=, 2000, in: books)
let long  = searchFor(property: \Book.pages, >=,  300, in: books)
let short = searchFor(property: \Book.pages,  <,  300, in: books)
let _1988 = searchFor(property: \Book.year,  ==, 1988, in: books)
    
print("New books: \(new.depiction)")
print("Long books: \(long.depiction)")
print("Short books: \(short.depiction)")
print("Books from 1988: \(_1988.depiction)\n")

print("Ordered array by year: \( books.sorted(by: \Book.year ).depiction)\n")
print("Ordered array by pages: \(books.sorted(by: \Book.pages).depiction)\n")

print("\nLet's reverse them -->\n")

print("Ordered array by year: \( books.sorted(by: \Book.year,  >).depiction)\n")
print("Ordered array by pages: \(books.sorted(by: \Book.pages, >).depiction)\n")

I hope this has given you some insight into the use of key paths, and inspired. If you find this code useful, you're free to use it as you wish. 😀

Output:

New books: [
    Book(pages: 365, year: 2002),
    Book(pages: 253, year: 2016),
    Book(pages: 100, year: 2017),
    Book(pages: 438, year: 2018),
    Book(pages: 284, year: 2010)
]
Long books: [
    Book(pages: 365, year: 2002),
    Book(pages: 438, year: 2018),
    Book(pages: 369, year: 1954),
    Book(pages: 312, year: 1988),
    Book(pages: 415, year: 1988)
]
Short books: [
    Book(pages: 253, year: 2016),
    Book(pages: 100, year: 2017),
    Book(pages: 92, year: 1993),
    Book(pages: 284, year: 1976),
    Book(pages: 284, year: 2010)
]
Books from 1988: [
    Book(pages: 312, year: 1988),
    Book(pages: 415, year: 1988)
]

Ordered array by year: [
    Book(pages: 369, year: 1954),
    Book(pages: 284, year: 1976),
    Book(pages: 312, year: 1988),
    Book(pages: 415, year: 1988),
    Book(pages: 92, year: 1993),
    Book(pages: 365, year: 2002),
    Book(pages: 284, year: 2010),
    Book(pages: 253, year: 2016),
    Book(pages: 100, year: 2017),
    Book(pages: 438, year: 2018)
]

Ordered array by pages: [
    Book(pages: 92, year: 1993),
    Book(pages: 100, year: 2017),
    Book(pages: 253, year: 2016),
    Book(pages: 284, year: 1976),
    Book(pages: 284, year: 2010),
    Book(pages: 312, year: 1988),
    Book(pages: 365, year: 2002),
    Book(pages: 369, year: 1954),
    Book(pages: 415, year: 1988),
    Book(pages: 438, year: 2018)
]


Let's reverse them -->

Ordered array by year: [
    Book(pages: 438, year: 2018),
    Book(pages: 100, year: 2017),
    Book(pages: 253, year: 2016),
    Book(pages: 284, year: 2010),
    Book(pages: 365, year: 2002),
    Book(pages: 92, year: 1993),
    Book(pages: 312, year: 1988),
    Book(pages: 415, year: 1988),
    Book(pages: 284, year: 1976),
    Book(pages: 369, year: 1954)
]

Ordered array by pages: [
    Book(pages: 438, year: 2018),
    Book(pages: 415, year: 1988),
    Book(pages: 369, year: 1954),
    Book(pages: 365, year: 2002),
    Book(pages: 312, year: 1988),
    Book(pages: 284, year: 1976),
    Book(pages: 284, year: 2010),
    Book(pages: 253, year: 2016),
    Book(pages: 100, year: 2017),
    Book(pages: 92, year: 1993)
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment