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)])
}
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. 😀
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)
]