Leave a rating/review
Notes: 17. filter, reduce, & sort
More About Algorithms
- Course: “Data Structures & Algorithms in Swift” (https://www.raywenderlich.com/977854-data-structures-algorithms-in-swift)
- Book: “Data Structures & Algorithms in Swift” (https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift)
- “Swift Algorithm Club” (https://github.com/raywenderlich/swift-algorithm-club)
Update Notes: This course was originally recorded in 2019. It has been reviewed and all content and materials updated as of October 2021.
This is the last episode that will focus on using closures and collections together, and here, we’ll tackle filter, reduce, and the sort methods.
Before the last challenge, I left you with this use of flatMap. We were trying to flatten this multi-dimensional array of String arrays into one array, and only keep the dwarf names that come after “M” in the alphabet.
I told you there was another way to do that second thing, and that way is to use filter!
filter is another extremely useful method that iterates over a collection and only returns the values that meet the criteria you want.
filter takes one closure as a parameter. That closure takes in one element from the collection, and returns a Boolean.
When you call filter on a collection, the closure’s job is to look at each element in that collection, and decide whether or not it should be included in the return value. The element is only added to the return value if the closure returns true.
Let’s try refactoring our previous code to use filter on those arrays of dwarf names, instead of a for loop.
Here’s our code from a previous episode, where we were flattening a multi-dimensional array of dwarf arrays. We can replace all of this code in the closure body with filter!
I’ll actually copy this whole thing, and paste it below, then comment out the original, and keep it as a reference for now. Ok! Start by calling filter on dwarves.
dwarves.filter(<#T##isIncluded: (String) throws -> Bool##(String) throws -> Bool#>)
You can see the closure argument is called isIncluded. Go ahead and hit return to let the closure autocomplete.
dwarves.filter { (<#String#>) -> Bool in
<#code#>
}
dwarves is a String array, so the closure takes in a string. Let’s call it dwarf.
dwarves.filter { (😺dwarf🛑) -> Bool in
Remember, the point of this closure is to say whether each element is included in the final result. So we need to return a Bool from the closure.
In the for loop, we used a where clause that compared the element to “M”. We can use exactly the same comparison in the closure body.
dwarf > "M"
That’s it! Run the playground. And there’s our dwarves after M! If you wanted to shorten this closure up, you can remove the parameter name and return type…
dwarves.filter { ❌(dwarf) -> Bool in❌
dwarf > "M"
}
And just use “ $0 greater than M “
dwarves.filter { $0 > "M" }
Run it one more time and it should still work exactly the same.
Next up in our bag of trick is reduce. This one can be a little tougher to get a handle on, but you can do it! We’ll return to an example from earlier in this course.
We were trying to write a function that took in an array of grades, and figured out if the average grade was a passing grade or not. To get there, we needed to loop over all of the grades and add each of them to a variable to get a total.
reduce can do the same thing for us in one line of code!
The other methods we’ve been using all take a single closure argument, but reduce takes two arguments. The first one is a starting value, and the second is a closure.
The closure takes in the starting value, and an element of the collection. The closure’s job is to do something with those two values, and then return a new starting value. The updated starting value is passed back into the closure along with the next element in the collection. Let’s try this out in code!
OK! Here’s our old getPassStatus function. And inside is this for loop and variable combo that adds up all of the grades. Let’s try the same thing with reduce.
I’ll copy this whole thing, paste it below and, again, comment out the original. I’ll just leave it there for reference. Now, I’ll delete all of this…
var totalGrade = ❌0
for grade in grades {
totalGrade += grade
}❌
…and start replacing it by calling reduce on grades
var totalGrade = 😺grades.reduce(<#T##initialResult: Result##Result#>, <#T##nextPartialResult: (Result, Int) throws -> Result##(Result, Int) throws -> Result#>)
The first parameter is called the initialResult. That’s our starting value! We should start with 0, just like the variable we made to start the for loop.
grades.reduce(😺0🛑) {
}
Hit Return to let that closure argument auto-generate
let totalGrade = grades.reduce(0) { (<#Result#>, <#Int#>) -> Result in
<#code#>
}
The closure takes two values: the current value of that initialResult parameter, and an element from the array. Let’s call the first one total, since it will be our running total, and the second one grade
grades.reduce(0) { 😺total, grade🛑 -> Result
The return type for this closure should be the same type as the initial result. It doesn’t have to be the same as the array element, but in our case, it is!
grades.reduce(0) { total, grade -> 😺Int🛑 in
Now, in the for loop, we added the grade to the total. We should just do the same here, in the closure body.
total + grade
The result of this is returned from the closure to replace that initial result of 0, and then the whole thing starts over again. And that’s it! Try calling the function, passing in Ozma’s grades and whatever you’d like for the lowest passing grade.
getPassStatus(for: ozmaGrades, lowestPass: 60)
Run the playground to check the results… And Ozma has passed! If you wanted to shorten up this closure, you could get rid of these parameter names and the return type.
total would become $0 and grade would become $1. In this case, though, that may actually be more confusing.
So, undo all of that… If you wanted to really shorten it up, though, there’s another option that’s really commonly used and readable.
We’ve been writing all of these trailing closures. But remember that we can pass an existing function as an argument?
And, really, all we’re doing here is adding the two parameters together. Which means we can replace the entire closure with the plus symbol.
let totalGrade = grades.reduce(0, 😺+) //{ total, grade -> Int in
// total + grade
// }🛑
Run this function call one more time, to prove that worked. And Ozma is still passing!
There’s a variation on reduce that lets you reduce a collection into an array or dictionary.
Imagine you represent the stock in your cat toy shop with a dictionary, like this one. You use prices as keys, and the values are the number of items in stock at that price.
What if you wanted to find out the total value of items at each price, and store those values in an array?
let stockSums =
We might loop through the dictionary, multiply each key / value pair together, and append the result of each multiplication to an array. We can do that with reduce(into)
let stockSums = stock.reduce(into: <#T##Result#>, <#T##updateAccumulatingResult: (inout Result, (key: Double, value: Int)) throws -> ()##(inout Result, (key: Double, value: Int)) throws -> ()#>)
This first argument is, again, our starting value. This time, we want an empty array.
let stockSums = stock.reduce(into: 😺[]) { (<#inout Result#>, <#(key: Double, value: Int)#>) in
<#code#>
}
and the second argument is our closure. It takes in our starting value, and a tuple that holds the key and value for a single element in the stock dictionary.
Notice this Result is labeled as inout, and that the closure doesn’t return anything. Our empty array will actually be mutated in each iteration, so there’s no need to return a value from the closure.
reduce will return the final array at the end. Call the first parameter result, and the second item
let stockSums = stock.reduce(into: []) { (😺result, item🛑) in
To finish up, append the result with the item’s key multiplied by its value.
result.append(item.key * Double(item.value))
Run the playground to see to total values stored in an array!
I’ve got one last sort of method for you, and it will help you sort your collections.
These methods handle the “how” of sorting for you, so you don’t need to write your own sorting algorithms. You just need to tell the method what criteria you’re trying to sort by.
Are you sorting alphabetically? Backwards? By the number of nicknames someone has? As you might have guessed, you do this with a closure.
he closure’s job is tell the sort method how to compare two elements in a collection, by saying if the first element should go before the second element in the final, sorted version.
So, the closure will return a boolean where true means “yes, this first thing should go before this second thing” and false means “no, actually, the first thing should come after the second thing.”
I am not going to get into the details of how sorting algorithms work in this course, but we do have another course, a book, and an excellent free resource for learning more about Data Structures and Algorithms in Swift.
You can find links to all of those in the episode notes. But for now, let’s try sorting some collections!
Remember when we you combined sets with the union method, and how there was version called formUnion that mutated the original set? Well, you have similar options to sort your collections!
The first kind is called sort. It will actually change the collection you call it on. You may also hear this called “sorting in place”.
If you want to mutate this names array, use the sort method. This one doesn’t take any parameters, and will just sort your collection alphabetically from A to Z, or sequentially going up in value.
names.sort()
If you want to sort your collection in a different way, you can use sort(by) instead. This is where closures come in! sort(by) takes one parameter, a closure that takes two parameters and returns a boolean.
names.sort { (a, b) -> Bool in
}
Remember, this closure’s job is to say if the first element should come first in the array. If we wanted to sort from from Z to A, we would do that by returning true if a is greater than b.
a > b
}
names
If the first element comes after the second element, alphabetically, put that one into the array first.
Run the playground… Tadaa! Now the names array is sorted in reverse-alphabetical order.
And, again, because all we’re doing is directly comparing the two parameters, and returning a boolean, you should replace this closure with just the greater than comparison operator.
names.sort(by: >)
Ok! So, that’s great if you want to sort an array in place. If you, instead, want to create a new array filled with the sorted values of the old, unsorted array, use the second kind of sort method.
There are two of these, as well, called sorted and sorted(by)! Let’s use sorted(by) to sort the names from longest to shortest, with the help of the count property.
let longToShortNames = names.sorted {
}
I don’t find parameter names very helpful in closures like this, so I’m going to stick with the shorter syntax and use $0 and $1
let longToShortNames = names.sorted {
$0.count > $1.count
}
- Check out both arrays to see the difference we’ve made!
longToShortNames
names
Ok, just one more thing. You may have worked this out already, but you can chain a lot of these kinds of methods together.
For example, if you go back to the top of this playground… What if you wanted to sort these dwarves after M, just in regular alphabetical order.
Well, you can just put a call to sorted right here at the end of flatMap’s trailing closure!
dwarves.filter { $0 > "M" }
}.sorted()
Run the playground… and there’s our dwarves after M, sorted alphabetically!