Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Swift  Module 3 – Strings, text and collections

map, filter, reduce and lazy sequences

Write Swift loops as map, filter, reduce, compactMap and flatMap, rank with sorted, min and prefix, and count the closure calls a lazy pipeline saves.

  • Intermediate
  • 15 minutes
  • Examples run with Swift 6.4 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Transform collections with map, filter, reduce(into:), compactMap and flatMap
  • Sort, search and combine sequences with sorted(by:), min(by:), prefix and zip, breaking ties on purpose
  • Use lazy sequences to avoid intermediate arrays and stop early
  • Predict how many times the closures of an eager and a lazy pipeline run

Before you start

What you’ll learn · Before you start

What you will learn

  • Transform collections with map, filter, reduce(into:), compactMap and flatMap
  • Sort, search and combine sequences with sorted(by:), min(by:), prefix and zip, breaking ties on purpose
  • Use lazy sequences to avoid intermediate arrays and stop early
  • Predict how many times the closures of an eager and a lazy pipeline run

Before you start

On this page

Finding the first six in a 120-ball innings, a pipeline of filter and map ran its test on all 120 balls, while the same pipeline made lazy stopped after 10. Both gave the same answer, over 2; one did twelve times the work, and built an array of every six on the way.

Here you will replace loops with map, filter, reduce, compactMap and flatMap, rank and search with sorted, min and prefix, breaking ties on purpose, and count the A function written as code in braces that can be passed to another function, which runs it later: { $0.runs > 0 } takes one value, named $0, and says whether its runs are above 0. The module on closures covers them in full. calls that a lazy pipeline saves, and the ones it repeats.

map, filter and reduce replace three kinds of loop

Three loops come up again and again: one that changes every item, one that keeps some items, and one that adds them up. Each has a method that takes the loop’s body as a closure, the code in braces that the method runs for every item, with $0 standing for the item. This program works on the twelve balls of two overs:

transform.swift: map, filter and reduce on two overs Swift · transform.swift
// transform.swift: map, filter and reduce on the balls of two overs.
struct Ball {
    let over: Int
    let batter: String
    let runs: Int
}

let balls = [
    Ball(over: 1, batter: "Asha", runs: 1), Ball(over: 1, batter: "Ravi", runs: 4),
    Ball(over: 1, batter: "Ravi", runs: 0), Ball(over: 1, batter: "Ravi", runs: 2),
    Ball(over: 1, batter: "Ravi", runs: 1), Ball(over: 1, batter: "Asha", runs: 0),
    Ball(over: 2, batter: "Ravi", runs: 6), Ball(over: 2, batter: "Ravi", runs: 0),
    Ball(over: 2, batter: "Ravi", runs: 1), Ball(over: 2, batter: "Asha", runs: 4),
    Ball(over: 2, batter: "Asha", runs: 0), Ball(over: 2, batter: "Asha", runs: 1),
]

let runs = balls.map(\.runs)
print("runs:", runs)
let scoring = balls.filter { $0.runs > 0 }.map(\.runs)
print("scoring balls:", scoring.count, "of", balls.count, scoring)
let total = scoring.reduce(0, +)
print("total:", total)

let byBatter = balls.reduce(into: [String: Int]()) { totals, ball in
    totals[ball.batter, default: 0] += ball.runs
}
for (batter, score) in byBatter.sorted(by: { $0.key < $1.key }) {
    print(batter, score)
}
let boundaries = balls.filter { $0.runs >= 4 }.map { "\($0.batter) \($0.runs)" }
print("boundaries:", boundaries)

Output

runs: [1, 4, 0, 2, 1, 0, 6, 0, 1, 4, 0, 1]
scoring balls: 8 of 12 [1, 4, 2, 1, 6, 1, 4, 1]
total: 20
Asha 6
Ravi 14
boundaries: ["Ravi 4", "Ravi 6", "Asha 4"]

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O transform.swift -o transform && ./transform

map returns a new array with the closure’s result for every item: map(\.runs) takes each ball’s runs, because a key path: An expression such as \Ball.runs, or \.runs where the type is known, that names a property. It reads that property from any value of the type, and can stand for a function, as in map(\.runs). can stand for the function that reads it (SE-0249). filter keeps the items for which the closure returns true, and reduce(0, +) starts from 0 and adds each item in turn. A closure that is the last argument may follow the parentheses, so filter { $0.runs > 0 } is filter({ $0.runs > 0 }). Step through the chain that kept the scoring balls and added them up:

Interactive · stage by stage filter, then map, then reduce on two overs Step through the chain in transform.swift. Each stage takes the whole result of the stage before it and returns a new one.
filter, then map, then reduce on two overs, step 5 of 5. Showing balls (12 balls, 4 of them dots), filter { $0.runs > 0 } (8 balls), map(\.runs) ([1, 4, 2, 1, 6, 1, 4, 1]), reduce(0, +) (total: 20).balls12 balls, 4 of them dotsfilter { $0.runs > 0 }8 ballsmap(\.runs)[1, 4, 2, 1, 6, 1, 4, 1]reduce(0, +)total: 20

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Twelve balls. balls holds the twelve balls of two overs, each with its over, its batter and its runs.
  2. filter keeps eight. filter runs its closure on all 12 balls and returns a new array of the 8 for which runs > 0 is true. The four dot balls are left out.
  3. map takes the runs. map(\.runs) calls the key path on each of the 8 balls and returns a new array of numbers: 1, 4, 2, 1, 6, 1, 4, 1.
  4. reduce adds them. reduce(0, +) starts from 0 and adds each number in turn: 1, 5, 7, 8, 14, 15, 19, 20. The result is one value, 20.
  5. Two arrays, then one answer. filter and map each built a whole new array before the next stage began, 8 balls and then 8 numbers, and reduce turned the numbers into one value. The lazy section shows a way to skip those arrays.

reduce(into:) suits a result that is itself a collection: its closure gets the result so far as an inout parameter and changes it in place, here a dictionary of runs by batter. The documentation of reduce(into:) recommends it over reduce(_:_:) whenever the result is a copy-on-write type such as an Array or a Dictionary.

Building an array with reduce(_:_:)

scores.reduce([]) { $0 + [$1] } makes a new array at every step, copying everything collected so far, so n items cost about n²/2 copies. reduce(into: []) { $0.append($1) } appends in place, and map or filter is clearer still when one of them says what you mean.

Checkpoint

Check: map, filter and reduce

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    print([1, 2, 3].map { $0 * 10 })
    Show the answer: Question 1 of 2

    Answer: [10, 20, 30]

    map is the loop that changes every item: one result per element, in the same order.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    let words = ["pen", "pad", "pen"]
    let counts = words.reduce(into: [String: Int]()) { $0[$1, default: 0] += 1 }
    print(counts["pen"] ?? 0, counts.count)
    Show the answer: Question 2 of 2

    Answer: 2 2

    reduce(into:) gives the closure the dictionary as an inout value, so each word updates it in place, which the documentation recommends for copy-on-write results.

compactMap drops the nils and flatMap joins the pieces

Scores typed by hand include mistakes: W for a wicket, an empty field, a word. Int("W") is nil, so mapping with Int(_:) gives an array of optionals, and compactMap keeps only the values that are there:

compact_flat.swift: compactMap on typed scores, flatMap on overs Swift · compact_flat.swift
// compact_flat.swift: compactMap drops the nils, flatMap joins nested arrays.
let typed = ["4", "W", "6", "", "1", "two"]
let parsed = typed.map { Int($0) }
print("map:", parsed)
let scores = typed.compactMap { Int($0) }
print("compactMap:", scores)

let overs = [[1, 4, 0, 2, 1, 0], [6, 0, 1, 4, 0, 1]]
print("map:", overs.map { $0.reduce(0, +) })
print("flatMap:", overs.flatMap { $0 })
print("flatMap, then filter:", overs.flatMap { $0 }.filter { $0 >= 4 })

Output

map: [Optional(4), nil, Optional(6), nil, Optional(1), nil]
compactMap: [4, 6, 1]
map: [8, 12]
flatMap: [1, 4, 0, 2, 1, 0, 6, 0, 1, 4, 0, 1]
flatMap, then filter: [4, 6, 4]

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O compact_flat.swift -o compact_flat && ./compact_flat

Step through what compactMap does with each of the six inputs:

Interactive · step by step compactMap on six typed scores Step through compactMap { Int($0) }. The left column is each input with what Int(_:) returns for it; the right column is the array that compactMap builds.
compactMap on six typed scores, step 6 of 6. Showing "4", "W", "6", "", "1", "two", 4, 6, 1."4"Int("4") = 4"W"Int("W") = nil"6"Int("6") = 6""Int("") = nil"1"Int("1") = 1"two"Int("two") = nil4result[0]6result[1]1result[2]

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Six strings. typed holds six strings. Int(_:) turns a string of digits into an Int and returns nil for anything else.
  2. "4" gives 4. The closure returns Optional(4). It holds a value, so compactMap unwraps it and appends 4.
  3. "W" gives nil. W is a wicket, not a number, so Int("W") is nil. compactMap drops it; map would have kept a nil in its place.
  4. "6" gives 6. 6 parses, so it is appended as the second value.
  5. The rest. The empty string and "two" give nil and are dropped; "1" gives 1, the third value.
  6. [4, 6, 1]. compactMap returned [4, 6, 1]: the values that were there, in their order, as plain Ints with no optionals left.

map kept a slot for every input, three of them nil; compactMap returned [4, 6, 1], the parsed numbers in their order. flatMap is the same idea for closures that return arrays: it joins the pieces, so the two overs became one list of twelve balls, which filter then searched for boundaries. map on the same overs gave one total per over instead, 8 and 12. Choose by the shape you want back: one result per item, the results that are not nil, or every element of every result.

Checkpoint

Check: compactMap and flatMap

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    print(["7", "x", "08"].compactMap { Int($0) })
    Show the answer: Question 1 of 2

    Answer: [7, 8]

    compactMap keeps the non-nil results of its closure, unwrapped, in their order.

  2. Question 2 of 2 What does [[1, 2], [], [3]].flatMap { $0 } return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: [1, 2, 3]

    flatMap is map followed by joining: every element of every returned sequence, in order.

sorted, min, max and prefix answer questions about order

sorted(by:) returns a new array ordered by a closure that says whether its first argument belongs before its second; sort(by:) sorts an array in place. This program ranks five players by runs:

ordering.swift: ranking with a tie-breaker, min, prefix, first(where:) and zip Swift · ordering.swift
// ordering.swift: sorted with a tie-breaker, min and max, prefix, first(where:) and zip.
let players = [("Asha", 45), ("Ravi", 52), ("Meera", 45), ("Kabir", 52), ("Zoya", 38)]

let byRuns = players.sorted { $0.1 > $1.1 }
print("by runs:", byRuns.map(\.0))
let ranked = players.sorted { $0.1 != $1.1 ? $0.1 > $1.1 : $0.0 < $1.0 }
print("by runs, then name:", ranked.map(\.0))

print("top two:", ranked.prefix(2).map(\.0))
print("lowest:", players.min { $0.1 < $1.1 }?.0 ?? "none")
print("first under 40:", players.first { $0.1 < 40 }?.0 ?? "none")
print("ranked while 50 or more:", ranked.prefix(while: { $0.1 >= 50 }).map(\.0))

let positions = zip(1..., ranked).map { "\($0). \($1.0)" }
print(positions)
let numbered = ranked.enumerated()
print("count:", numbered.count, "last:", numbered.last.map { "\($0.offset) \($0.element.0)" } ?? "none")

Output

by runs: ["Ravi", "Kabir", "Asha", "Meera", "Zoya"]
by runs, then name: ["Kabir", "Ravi", "Asha", "Meera", "Zoya"]
top two: ["Kabir", "Ravi"]
lowest: Zoya
first under 40: Zoya
ranked while 50 or more: ["Kabir", "Ravi"]
["1. Kabir", "2. Ravi", "3. Asha", "4. Meera", "5. Zoya"]
count: 5 last: 4 Zoya

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O ordering.swift -o ordering && ./ordering

Sorting by runs alone put Ravi before Kabir and Asha before Meera, the order they had in the input. Swift’s sort is stable sort: A sort that keeps elements its comparison calls equal in the order they had before. Swift documents sort(by:) and sorted(by:) as stable, so a tie keeps its input order.: the sorted(by:) documentation says it “preserves the relative order of elements” that the closure does not order (SE-0372). A ranking that should not depend on input order needs a tie-breaker, here the name. Step through both sorts:

Interactive · step by step Sorting five players, with and without a tie-breaker Step through both sorts. The left column is the players in input order; the right column is the ranking each sort returns.
Sorting five players, with and without a tie-breaker, step 5 of 5. Showing Asha 45, Ravi 52, Meera 45, Kabir 52, Zoya 38, 1st (Kabir 52), 2nd (Ravi 52), 3rd (Asha 45), 4th (Meera 45), 5th (Zoya 38). In focus: 1st, 2nd.Asha 45input 1Ravi 52input 2Meera 45input 3Kabir 52input 4Zoya 38input 51stKabir 522ndRavi 523rdAsha 454thMeera 455thZoya 38

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Two ties. Ravi and Kabir both have 52, and Asha and Meera both have 45. The closure { $0.1 > $1.1 } says nothing about the order inside a tie.
  2. By runs alone. sorted is stable, so each tie keeps the input order: Ravi came before Kabir, and Asha before Meera.
  3. Change the input. Had the input listed Kabir before Ravi, the same sort would rank Kabir first. The ranking depends on where the data came from.
  4. Add a tie-breaker. The second sort compares names when the runs are equal: $0.1 != $1.1 ? $0.1 > $1.1 : $0.0 < $1.0. Kabir now comes before Ravi, and Asha before Meera, whatever the input order.
  5. Then prefix and zip. prefix(2) takes Kabir and Ravi, and zip(1..., ranked) numbers the ranking from 1.

prefix(2) took the top two, min(by:) found the lowest score and returns an optional, nil for an empty array, and first(where:) and prefix(while:) stop at the first item that settles the question. zip(1..., ranked) numbered the ranking by pairing it with a one-sided range, and enumerated() numbered it from 0.

Two compile-time surprises come with these tools. Since Swift 6.2 (SE-0459) the result of enumerated() on an array is a collection too, so .count and .last work on it, but on macOS that conformance needs version 26 at run time. And KeyPathComparator, a comparator that sorts by a key path, belongs to Foundation, not to the standard library. This session builds the three files below:

The three files the session builds

enumerated_old.swift

// enumerated_old.swift: the result of enumerated() used as a Collection.
let numbered = ["Kabir", "Ravi", "Asha"].enumerated()
print(numbered.count)

keypath_sort.swift

// keypath_sort.swift: KeyPathComparator without import Foundation.
struct Player {
    let name: String
    let runs: Int
}
let players = [Player(name: "Asha", runs: 45), Player(name: "Ravi", runs: 52)]
print(players.sorted(using: KeyPathComparator(\Player.runs, order: .reverse)).map { $0.name })

keypath_fixed.swift

// keypath_fixed.swift: the same sort with import Foundation.
import Foundation

struct Player {
    let name: String
    let runs: Int
}
let players = [Player(name: "Asha", runs: 45), Player(name: "Ravi", runs: 52)]
print(players.sorted(using: KeyPathComparator(\Player.runs, order: .reverse)).map { $0.name })
enumerated() for an older macOS, and KeyPathComparator without and with Foundation Python · compile_session.py
from session import run, version

# enumerated_old.swift built for macOS 15 in both language modes, then for this Mac; KeyPathComparator without and
# with Foundation.
version()
run("""
swiftc -target arm64-apple-macosx15.0 enumerated_old.swift -o enumerated_old
!swiftc -swift-version 6 -target arm64-apple-macosx15.0 enumerated_old.swift -o enumerated_old
swiftc enumerated_old.swift -o enumerated_old
./enumerated_old
!swiftc keypath_sort.swift -o keypath_sort
swiftc keypath_fixed.swift -o keypath_fixed
./keypath_fixed
""")

Output

# Apple Swift version 6.4 (swiftlang-6.4.0.34.1)
$ swiftc -target arm64-apple-macosx15.0 enumerated_old.swift -o enumerated_old
enumerated_old.swift:3:16: warning: conformance of 'EnumeratedSequence<Base>' to 'Collection' is only available in macOS 26.0 or newer; this is an error in the Swift 6 language mode
1 | // enumerated_old.swift: the result of enumerated() used as a Collection.
2 | let numbered = ["Kabir", "Ravi", "Asha"].enumerated()
3 | print(numbered.count)
  |                |- warning: conformance of 'EnumeratedSequence<Base>' to 'Collection' is only available in macOS 26.0 or newer; this is an error in the Swift 6 language mode
  |                `- note: add 'if #available' version check
4 | 
$ swiftc -swift-version 6 -target arm64-apple-macosx15.0 enumerated_old.swift -o enumerated_old
enumerated_old.swift:3:16: error: conformance of 'EnumeratedSequence<Base>' to 'Collection' is only available in macOS 26.0 or newer
1 | // enumerated_old.swift: the result of enumerated() used as a Collection.
2 | let numbered = ["Kabir", "Ravi", "Asha"].enumerated()
3 | print(numbered.count)
  |                |- error: conformance of 'EnumeratedSequence<Base>' to 'Collection' is only available in macOS 26.0 or newer
  |                `- note: add 'if #available' version check
4 | 
[exit status 1]
$ swiftc enumerated_old.swift -o enumerated_old
$ ./enumerated_old
3
$ swiftc keypath_sort.swift -o keypath_sort
keypath_sort.swift:7:29: error: cannot find 'KeyPathComparator' in scope
5 | }
6 | let players = [Player(name: "Asha", runs: 45), Player(name: "Ravi", runs: 52)]
7 | print(players.sorted(using: KeyPathComparator(\Player.runs, order: .reverse)).map { $0.name })
  |                             `- error: cannot find 'KeyPathComparator' in scope
8 | 
[exit status 1]
$ swiftc keypath_fixed.swift -o keypath_fixed
$ ./keypath_fixed
["Ravi", "Asha"]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compile_session.py

Built for macOS 15, numbered.count is a warning in the Swift 5 language mode and an error in Swift 6 mode; built for this Mac’s macOS 26, it prints 3. Without import Foundation the compiler cannot find KeyPathComparator; with it, the sort works.

Key idea

Sorting is stable, so equal items keep their input order: when a ranking must not depend on that order, sort by a second key that breaks every tie.

Checkpoint

Check: Sorting, ties and stopping early

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    let entries = [("b", 1), ("a", 2), ("c", 1)]
    print(entries.sorted { $0.1 < $1.1 }.map(\.0))
    Show the answer: Question 1 of 2

    Answer: ["b", "c", "a"]

    sorted(by:) preserves the relative order of elements its closure does not order. Add a tie-breaker when the input order should not decide.

  2. Question 2 of 2 What does [Int]().min() return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: nil

    min(), max(), min(by:) and max(by:) all return optionals, as first and last do.

A lazy pipeline works one item at a time, only when asked

An eager filter runs its closure on every item and returns a new array before the next step starts. .lazy gives a lazy sequence: A sequence, made with .lazy, whose map, filter and similar operations store their closures and run them only when an element is read. It builds no arrays on the way, and runs the closures again on every read., whose map and filter store their closures instead, and run them only when someone asks for an element. This program finds the over of the first six in 120 balls both ways, and counts the calls:

lazy_counts.swift: closure calls of an eager and a lazy pipeline Swift · lazy_counts.swift
// lazy_counts.swift: how often the closures of an eager and a lazy pipeline run.
let pattern = [0, 1, 0, 4, 1, 0, 2, 0, 1, 6, 0, 1]
let runs = (0..<120).map { pattern[$0 % pattern.count] }  // 120 balls: 20 overs of 6

var sixChecks = 0
var overCalls = 0
let isSix = { (ball: Int) -> Bool in
    sixChecks += 1
    return runs[ball] == 6
}
let overOf = { (ball: Int) -> Int in
    overCalls += 1
    return ball / 6 + 1
}

let eager = runs.indices.filter(isSix).map(overOf).first
print("eager: first six in over", eager ?? 0, "| isSix ran", sixChecks, "| overOf ran", overCalls)

sixChecks = 0
overCalls = 0
let lazy = runs.indices.lazy.filter(isSix).map(overOf).first
print("lazy:  first six in over", lazy ?? 0, "| isSix ran", sixChecks, "| overOf ran", overCalls)

sixChecks = 0
let sixes = runs.indices.lazy.filter(isSix)
print("sixes:", sixes.count, "| isSix ran", sixChecks)
print("sixes again:", Array(sixes).count, "| isSix ran", sixChecks)

Output

eager: first six in over 2 | isSix ran 120 | overOf ran 10
lazy:  first six in over 2 | isSix ran 10 | overOf ran 1
sixes: 10 | isSix ran 120
sixes again: 10 | isSix ran 240

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O lazy_counts.swift -o lazy_counts && ./lazy_counts

The eager version checked all 120 balls, built an array of the 10 sixes and worked out the over of each, only to keep the first. The lazy version checked balls until it found the first six, at ball 10, and called overOf once. Step through how first pulls one ball at a time through the lazy pipeline:

Interactive · message by message How first pulls balls through a lazy pipeline Step through the lazy search of lazy_counts.swift. first asks the lazy pipeline for one element, and the pipeline asks the balls for one index at a time.
How first pulls balls through a lazy pipeline, step 6 of 6. Messages so far: first to lazy filter and map: next element?; runs.indices to lazy filter and map: index 0; runs.indices to lazy filter and map: indices 1 to 8; runs.indices to lazy filter and map: index 9; lazy filter and map to first: over 2.firstover 2lazy filter and mapisSix 10 times, overOf onceruns.indiceseager: read all 120next element?index 0indices 1 to 8index 9over 2

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. first asks. first needs one element, so it asks the lazy pipeline for its next one. Nothing has run yet: making the pipeline ran no closure.
  2. Ball 0. The filter asks the indices for one, gets 0 and runs isSix(0). Ball 0 is a dot, so the filter asks again.
  3. Balls 1 to 8. Balls 1 to 8 are not sixes either. isSix has now run 9 times, and overOf not at all.
  4. Ball 9 is a six. Index 9 holds the first 6, so the filter passes it to map, which runs overOf(9) once: 9 / 6 + 1 is over 2.
  5. first is done. The pipeline hands over 2 to first, which stops asking. Indices 10 to 119 are never read, and no array was built.
  6. The eager way. The eager filter ran isSix on all 120 balls and built an array of 10 sixes, then map ran overOf 10 times, and first kept only the first result.

The LazySequenceProtocol documentation describes exactly this: each time an element of a lazy sequence is accessed, its closure runs on an element of the base. That cuts both ways. The last two lines read the same lazy filter twice, once for count and once to build an array, and isSix ran 120 times for each, 240 in all. A lazy result is a recipe, not a stored answer.

Reading a lazy sequence again and again

Every pass over a lazy map or filter runs its closures again, and a closure with a side effect, such as a counter or a log line, repeats it. Turn the result into an Array once, let sixes = Array(…lazy.filter(isSix)), when you will read it more than once.

Checkpoint

Check: Lazy pipelines and their closure calls

  1. Question 1 of 2 How many times does the closure run in [0, 1, 0, 4, 1, 6].lazy.filter { $0 >= 4 }.first?

    Type a number.

    A hint

    first stops at the first element that passes.

    Show the answer: Question 1 of 2

    Answer: 4 calls

    The lazy filter tests 0, 1, 0 and then 4, which passes, and first stops there: 4 calls. The eager filter would test all 6 elements first.

  2. Question 2 of 2 doubled = marks.lazy.map { slowScore($0) }, where marks has 50 items. The code reads doubled.first, then loops over doubled twice. How many times does slowScore run?

    Type a number.

    A hint

    Every read of an element runs the closure again.

    Show the answer: Question 2 of 2

    Answer: 101 calls

    first maps one element, 1 call, and each full loop maps all 50 again: 1 + 50 + 50 = 101. Array(doubled), made once, would cost 50 calls in all.

Practise: statistics from an innings

From the balls of three overs, print each batter’s runs and strike rate (runs per 100 balls, to one decimal), every batter with the top score, ties included, and the first over in which 15 or more runs were scored. Write each answer as a pipeline: reduce(into:) for the totals, map and sorted for the report, filter for the ties and first(where:) for the over.

Write and test yours before reading this version:

practice.swift: one way to write it Swift · practice.swift
// practice.swift: strike rates, top scorers with ties and the first big over, written as pipelines.
struct Ball {
    let over: Int
    let batter: String
    let runs: Int
}

let pattern: [(String, Int)] = [
    ("Asha", 1), ("Ravi", 4), ("Ravi", 0), ("Ravi", 1), ("Asha", 2), ("Asha", 0),
    ("Meera", 6), ("Meera", 1), ("Asha", 4), ("Asha", 0), ("Asha", 1), ("Meera", 0),
    ("Ravi", 6), ("Ravi", 4), ("Ravi", 1), ("Meera", 2), ("Meera", 4), ("Meera", 3),
]
let innings = pattern.enumerated().map { Ball(over: $0.offset / 6 + 1, batter: $0.element.0, runs: $0.element.1) }

let faced = innings.reduce(into: [String: (runs: Int, balls: Int)]()) { tally, ball in
    tally[ball.batter, default: (0, 0)].runs += ball.runs
    tally[ball.batter, default: (0, 0)].balls += 1
}
let rates = faced.map { batter, t in (batter, t.runs, (t.runs * 1000 + t.balls / 2) / t.balls) }
for (batter, runs, tenths) in rates.sorted(by: { $0.0 < $1.0 }) {
    print(batter, runs, "runs, strike rate \(tenths / 10).\(tenths % 10)")
}

let best = faced.values.map(\.runs).max() ?? 0
print("top score \(best):", faced.filter { $0.value.runs == best }.keys.sorted())

let overTotals = Dictionary(grouping: innings, by: \.over).mapValues { $0.map(\.runs).reduce(0, +) }
let firstBig = overTotals.keys.sorted().lazy.first { overTotals[$0, default: 0] >= 15 }
print("over totals:", overTotals.sorted(by: { $0.key < $1.key }).map(\.value))
print("first over of 15 or more:", firstBig.map(String.init) ?? "none")

Output

Asha 8 runs, strike rate 133.3
Meera 16 runs, strike rate 266.7
Ravi 16 runs, strike rate 266.7
top score 16: ["Meera", "Ravi"]
over totals: [8, 12, 20]
first over of 15 or more: 3

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O practice.swift -o practice && ./practice

reduce(into:) collects runs and balls per batter in one pass over the innings, and the strike rate is worked out in tenths with integers, so 16 runs from 6 balls prints 266.7. Meera and Ravi share the top score, so filter keeps both, sorted by name. Over 3 is the first with 15 or more runs, 20. The practice round below picks your weakest idea first, and each right answer brings a question one level up:

Practice that adapts to you

Practise this lesson

13 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 What does filter return?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: A new array of the elements for which the closure returns true, in their order

    filter is the loop that keeps some items.

  2. Question 2 of 13 What does this print?

    Read the code, then choose one answer.

    struct T { let n: Int }
    print([T(n: 3), T(n: 5)].map(\.n).reduce(0, +))
    Show the answer: Question 2 of 13

    Answer: 8

    \.n works wherever a function from T to Int is expected, so map(\.n) is map { $0.n }.

  3. Question 3 of 13 You build a dictionary from subject to marks, [String: [Int]], from 100,000 results. Which way does it in one pass without copying the dictionary at each step?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: results.reduce(into: [:]) { $0[$1.subject, default: []].append($1.mark) }

    For a copy-on-write result, reduce(into:) is the efficient form, as its documentation says.

  4. Question 4 of 13 What is the type of ["4", "W"].map { Int($0) }?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: [Int?], an array of optional Ints

    map keeps whatever the closure returns, optionals included; compactMap unwraps and drops nils.

  5. Question 5 of 13 What does this print?

    Read the code, then choose one answer.

    let overs = [[1, 4], [6]]
    print(overs.map { $0.count }, overs.flatMap { $0 }.count)
    Show the answer: Question 5 of 13

    Answer: [2, 1] 3

    map keeps one result per element; flatMap joins sequences into one.

  6. Question 6 of 13 You need the valid ages from ["21", "-", "34"] as Ints. Which line gives them safely?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: ages.compactMap { Int($0) }

    compactMap is the transform and the check in one step; a default value hides bad input instead of dropping it.

  7. Question 7 of 13 What is the difference between sort() and sorted()?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: sorted() returns a new sorted array; sort() sorts a var array in place

    Use sorted() when you need the original too, and sort() to reorder a var in place.

  8. Question 8 of 13 Which closure ranks players by runs, highest first, and by name from A to Z when the runs are equal?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: { $0.runs != $1.runs ? $0.runs > $1.runs : $0.name < $1.name }

    A sort predicate must be a strict weak ordering, and a tie-breaker is a second comparison used only when the first says the elements are equal.

  9. Question 9 of 13 What does this print?

    Read the code, then choose one answer.

    print([52, 50, 45, 52].prefix(while: { $0 >= 50 }))
    Show the answer: Question 9 of 13

    Answer: [52, 50]

    prefix(while:) keeps the leading run of elements that pass, and stops early.

  10. Question 10 of 13 first(where:) runs on an array of 1,000,000 elements, and the first match is at position 10. How many elements does it test?

    Type a number.

    Show the answer: Question 10 of 13

    Answer: 11 elements

    It tests positions 0 to 10 and stops at the match: 11 tests, however long the array is.

  11. Question 11 of 13 What does .lazy change about map and filter?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: They store their closures and run them only when an element is asked for

    The LazySequenceProtocol documentation describes elements computed on demand.

  12. Question 12 of 13 In lazy_counts.swift, the eager pipeline runs.indices.filter(isSix).map(overOf).first runs on 120 balls with 10 sixes. How many times does overOf run?

    Type a number.

    Show the answer: Question 12 of 13

    Answer: 10 calls

    The eager filter returns all 10 sixes, and the eager map runs overOf on each of them before first takes one.

  13. Question 13 of 13 When can making a pipeline lazy make a program slower?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: When the result is read several times, because its closures run again on every read

    Use lazy for one pass that can stop early or avoid intermediate arrays; make an Array when the result is reused.

Interview questions

Warm-up: what is the difference between map, compactMap and flatMap?

All three call a closure on every element and return an array, and they differ in what they do with the closure’s results. map keeps one result per element, so mapping with a closure that returns an optional gives an array of optionals, such as [Optional(4), nil, Optional(6)]. compactMap unwraps the results and drops the nils, so ["4", "W", "6"].compactMap { Int($0) } is [4, 6]. flatMap is for closures that return a sequence: it joins all the returned sequences into one array, so [[1, 4], [6]].flatMap { $0 } is [1, 4, 6]. Older Swift also had a flatMap that removed nils; it was renamed compactMap (SE-0187) because the two jobs are different. A quick test is the shape you need back: one result per element, only the values that exist, or every element of every result.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A view model stores orders.lazy.filter(isSuspicious).map(enrich) in a property. The screen reads its count, then shows the list. Logs show enrich running twice for many orders, and isSuspicious, which calls a fraud service, uses up the service's quota early. What is happening, and how do you fix it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Rank players by runs, highest first, so that tied players share a rank, as in 1, 2, 2, 4, and players inside a tie are listed by name. Describe the Swift code, explain why the order must not depend on the input order, and give its cost.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • map transforms every element, filter keeps those that pass a test, and reduce combines them; reduce(into:) builds an array or dictionary in place, which the documentation recommends for copy-on-write results.
  • compactMap keeps only the non-nil results of a closure, and flatMap joins the sequences a closure returns.
  • sorted(by:) is stable, so a ranking that must not depend on input order needs a tie-breaker; min(by:) and max(by:) return optionals, and prefix, first(where:) and prefix(while:) stop early.
  • .lazy makes map and filter run their closures only when an element is asked for, so a search can stop at the first match without building arrays.
  • A lazy sequence runs its closures again on every pass, so turn it into an Array when it will be read more than once.

Terms in this lesson

key path
An expression such as \Ball.runs, or \.runs where the type is known, that names a property. It reads that property from any value of the type, and can stand for a function, as in map(\.runs).
lazy sequence
A sequence, made with .lazy, whose map, filter and similar operations store their closures and run them only when an element is read. It builds no arrays on the way, and runs the closures again on every read.
stable sort
A sort that keeps elements its comparison calls equal in the order they had before. Swift documents sort(by:) and sorted(by:) as stable, so a tie keeps its input order.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.