├── MapFilterReduce.playground ├── Resources │ ├── Map.png │ ├── Filter.png │ ├── Reduce.png │ └── XcodeQuickHelp.png ├── playground.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── contents.xcplayground ├── timeline.xctimeline └── Contents.swift ├── README.md └── LICENSE /MapFilterReduce.playground/Resources/Map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kharrison/MapFilterReduce/HEAD/MapFilterReduce.playground/Resources/Map.png -------------------------------------------------------------------------------- /MapFilterReduce.playground/Resources/Filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kharrison/MapFilterReduce/HEAD/MapFilterReduce.playground/Resources/Filter.png -------------------------------------------------------------------------------- /MapFilterReduce.playground/Resources/Reduce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kharrison/MapFilterReduce/HEAD/MapFilterReduce.playground/Resources/Reduce.png -------------------------------------------------------------------------------- /MapFilterReduce.playground/Resources/XcodeQuickHelp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kharrison/MapFilterReduce/HEAD/MapFilterReduce.playground/Resources/XcodeQuickHelp.png -------------------------------------------------------------------------------- /MapFilterReduce.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MapFilterReduce.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Swift Guide To Map Filter And Reduce 2 | 3 | A swift playground guide to using `map`, `filter`, `reduce`, and `flatmap`. See the original blog post: 4 | 5 | * [A Swift Guide to Map Filter Reduce](https://useyourloaf.com/blog/swift-guide-to-map-filter-reduce/) -------------------------------------------------------------------------------- /MapFilterReduce.playground/playground.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Keith Harrison 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MapFilterReduce.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 54 | 55 | 59 | 60 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /MapFilterReduce.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: ## Swift Guide To Map Filter Reduce 2 | //: Using `map`, `filter` or `reduce` to operate on Swift collection types such as `Array` or `Dictionary` is something that can take getting used to. Unless you have experience with functional languages your instinct may be to reach for the more familiar *for-in loop*. With that in mind here is my guide to using `map`, `filter`, `reduce` (and `flatMap` and `compactMap`). 3 | //: 4 | //: Original blog post: [Swift Guide To Map Filter Reduce](https://useyourloaf.com/blog/swift-guide-to-map-filter-reduce/) 5 | //: 6 | //: *Updated 1-May-2018 for Xcode 9.3 and Swift 4.1* 7 | 8 | import Foundation 9 | 10 | /*: 11 | ### Map 12 | 13 | **Use `map` to loop over a collection and apply the same operation to each element in the collection.** The `map` function returns an array containing the results of applying a mapping or transform function to each item: 14 | 15 | ![Swift map function](Map.png) 16 | 17 | We could use a for-in loop to compute the squares of each item in an array: 18 | */ 19 | let values = [2.0,4.0,5.0,7.0] 20 | var squares: [Double] = [] 21 | for value in values { 22 | squares.append(value*value) 23 | } 24 | /*: 25 | This works but the boilerplate code to declare the type of the `squares` array and then loop over it is a little verbose. We also need to make the `squares` array a var as we are changing it in the loop. Now compare to when we use `map`: 26 | */ 27 | let squares2 = values.map {$0 * $0} 28 | // [4.0, 16.0, 25.0, 49.0] 29 | /*: 30 | This is a big improvement. We don't need the for loop as `map` takes care of that for us. Also the `squares` result is now a let or non-mutating value and we did not even need to declare its type as Swift can infer it. 31 | 32 | **The shorthand closure syntax can make this hard to follow at first.** The `map` function has a single argument which is a closure (a function) that it calls as it loops over the collection. This closure takes the element from the collection as an argument and returns a result. The map function returns these results in an array. 33 | 34 | Writing the mapping function in long form can make it easier to see what is happening: 35 | */ 36 | let squares3 = values.map({ 37 | (value: Double) -> Double in 38 | return value * value 39 | }) 40 | /*: 41 | The closure has a single argument: `(value: Double)` and returns a `Double` but Swift can infer this. Also since `map` has a single argument which is a closure we do not need the `(` and `)` and with a single line closure we can even omit the `return`: 42 | */ 43 | let squares4 = values.map {value in value * value} 44 | /*: 45 | The `in` keyword separates the argument from the body of the closure. If you prefer you can go one step further and use the numbered arguments shorthand: 46 | */ 47 | let squares5 = values.map { $0 * $0 } 48 | /*: 49 | **The type of the results is not limited to the type of the elements in the original array.** Here is an example of mapping an array of integers to strings: 50 | */ 51 | let scores = [0,28,124] // TODO 52 | let words = scores.map { NumberFormatter.localizedString(from: $0 as NSNumber, number: .spellOut) } 53 | words 54 | // ["zero", "twenty-eight", "one hundred twenty-four"] 55 | /*: 56 | **The map operation is not limited to Arrays you can use it anywhere you have a collection type.** For example, use it with a `Dictionary` or a `Set`, the result will always be an `Array`. Here is an example with a Dictionary: 57 | */ 58 | let milesToPoint = ["point1":120.0,"point2":50.0,"point3":70.0] 59 | let kmToPoint = milesToPoint.map { name,miles in miles * 1.6093 } 60 | /*: 61 | **Quick tip: If you have trouble understanding the argument types of the closure Xcode code completion will help you:** 62 | 63 | ![Xcode quick help](XcodeQuickHelp.png) 64 | 65 | In this case we are mapping a `Dictionary` so as we iterate over the collection our closure has arguments that are a `String` and a `Double` from the types of the key and value that make up each element of the dictionary. 66 | */ 67 | /*: 68 | ### Filter 69 | 70 | **Use `filter` to loop over a collection and return an `Array` containing only those elements that match an include condition.** 71 | 72 | ![Swift filter function](Filter.png) 73 | 74 | The `filter` method has a single argument that specifies the include condition. This is a closure that takes as an argument the element from the collection and must return a `Bool` indicating if the item should be included in the result. 75 | 76 | An example that filters as array of integers returning only the even values: 77 | */ 78 | let digits = [1,4,10,15] 79 | let even = digits.filter { $0 % 2 == 0 } 80 | even 81 | // [4, 10] 82 | /*: 83 | ### Reduce 84 | 85 | **Use `reduce` to combine all items in a collection to create a single new value.** 86 | 87 | ![Swift reduce function](Reduce.png) 88 | 89 | The `reduce` method takes two values, an initial value and a combine closure. For example, to add the values of an array to an initial value of 10.0: 90 | */ 91 | let items = [2.0,4.0,5.0,7.0] 92 | let total = items.reduce(10.0,+) 93 | // 28.0 94 | /*: 95 | This will also work with strings using the `+` operator to concatenate: 96 | */ 97 | let codes = ["abc","def","ghi"] 98 | let text = codes.reduce("", +) 99 | // "abcdefghi" 100 | /*: 101 | The combine argument is a closure so you can also write reduce using the trailing closure syntax: 102 | */ 103 | let names = ["alan","brian","charlie"] 104 | let csv = names.reduce("===") {text, name in "\(text),\(name)"} 105 | // "===,alan,brian,charlie" 106 | /*: 107 | ### FlatMap and CompactMap 108 | 109 | These are variations on the plain `map` that flatten or compact the result. There are three situations where they apply: 110 | 111 | **1. Using `FlatMap` on a sequence with a closure that returns a sequence:** 112 | 113 | `Sequence.flatMap(_ transform: (Element) -> S) 114 | -> [S.Element] where S : Sequence` 115 | 116 | I think this was probably the first use of `flatMap` I came across in Swift. Use it to apply a closure to each element of a sequence and flatten the result: 117 | */ 118 | let results = [[5,2,7], [4,8], [9,1,3]] 119 | let allResults = results.flatMap { $0 } 120 | // [5, 2, 7, 4, 8, 9, 1, 3] 121 | 122 | let passMarks = results.flatMap { $0.filter { $0 > 5} } 123 | // [7, 8, 9] 124 | /*: 125 | **2. Using `FlatMap` on an optional:** 126 | 127 | The closure takes the non-nil value of the optional and returns an optional. If the original optional is `nil` then `flatMap` returns `nil`: 128 | 129 | `Optional.flatMap(_ transform: (Wrapped) -> U?) -> U?` 130 | */ 131 | let input: Int? = Int("8") 132 | let passMark: Int? = input.flatMap { $0 > 5 ? $0 : nil} 133 | /*: 134 | **3. Using `CompactMap` on a sequence with a closure that returns an optional:** 135 | 136 | `Sequence.compactMap(_ transform: (Element) -> U?) -> U?` 137 | 138 | Note that this use of `flatMap` was renamed to `compactMap` in Swift 4.1 (Xcode 9.3). It provides a convenient way to strip `nil` values from an array: 139 | */ 140 | let keys: [String?] = ["Tom", nil, "Peter", nil, "Harry"] 141 | let validNames = keys.compactMap { $0 } 142 | validNames 143 | // ["Tom", "Peter", "Harry"] 144 | let counts = keys.compactMap { $0?.count } 145 | counts 146 | // [3, 5, 5] 147 | /*: 148 | See also: [Replacing flatMap with compactMap](https://useyourloaf.com/blog/replacing-flatmap-with-compactmap/). 149 | */ 150 | /*: 151 | ### Chaining 152 | 153 | You can chain methods. For example to sum only those numbers greater than or equal to seven we can first filter and then reduce: 154 | */ 155 | let marks = [4,5,8,2,9,7] 156 | let totalPass = marks.filter{$0 >= 7}.reduce(0, +) 157 | totalPass 158 | // 24 159 | /*: 160 | Another example that returns only the even squares by first filtering the odd values and then mapping the remaining values to their squares (as pointed out in the comments filtering first avoids mapping the odd values which always give odd squares): 161 | */ 162 | let numbers = [20,17,35,4,12] 163 | let evenSquares = numbers.filter{$0 % 2 == 0}.map{$0 * $0} 164 | evenSquares 165 | // [400, 16, 144] 166 | /*: 167 | ### Quick Summary 168 | 169 | Next time you find yourself looping over a collection check if you could use map, filter or reduce: 170 | 171 | + `map` returns an `Array` containing results of applying a transform to each item. 172 | + `filter` returns an `Array` containing only those items that match an include condition. 173 | + `reduce` returns a single value calculated by calling a combine closure for each item with an initial value. 174 | */ 175 | --------------------------------------------------------------------------------