Skip to content

Add rule: prefer lazy.map before single-pass operations - #402

Merged
jparise merged 4 commits into
masterfrom
prefer-lazy-map
Aug 21, 2026
Merged

Add rule: prefer lazy.map before single-pass operations#402
jparise merged 4 commits into
masterfrom
prefer-lazy-map

Conversation

@jparise

@jparise jparise commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Enables SwiftFormat's preferLazyMap rule and documents the corresponding style-guide entry (a sibling of the existing preferMinOverSorted, preferFirstWhere, and preferContains collection rules).

Reasoning

map allocates an array of every transformed element. When all that happens to that array is a single walk over it, that allocation is waste — lazy.map transforms each element as it's visited instead.

// WRONG
let names = users.map { $0.name }.joined(separator: ", ")
let minY = vertices.map { $0.y }.min()

// RIGHT
let names = users.lazy.map { $0.name }.joined(separator: ", ")
let minY = vertices.lazy.map { $0.y }.min()

Qualifying operations consume their receiver exactly once (min, max, reduce, joined(separator:)) or can additionally stop early and skip transforming the rest (contains, allSatisfy, first(where:)). The style-guide entry notes the converse too: lazy is only right when the result really is consumed once, since sorted() materializes anyway and anything walking the result twice redoes the transform.

Enables SwiftFormat's `preferLazyMap` rule and documents the corresponding
style-guide entry (a sibling of the existing `preferMinOverSorted`,
`preferFirstWhere`, and `preferContains` collection rules). Also bumps the
SwiftFormat artifactbundle to the `2026-08-20-b` nightly, the first build that
includes the rule along with its follow-up correctness fixes.

`map` allocates an array of every transformed element. When all that happens to
that array is a single walk over it, the allocation is waste: `lazy.map`
transforms each element as it is visited. The qualifying operations consume their
receiver exactly once — `min`, `max`, `reduce`, `joined(separator:)` — plus
`contains`, `allSatisfy`, and `first(where:)`, which can stop early and skip
transforming the rest.

Enabling the rule autocorrected nothing in this repo; its six Swift files have no
eligible sites.
Comment thread README.md Outdated
`contains` shows the larger of the two wins, so it goes first: the eager version
transforms every element before looking at any of them, even when the answer is
the first one. Keeping a `min` example alongside it covers the plain
allocation-avoidance case, and dropping the third example leaves the block
easier to read. The list of qualifying operations moves below the examples,
where it reads as a reference rather than a preamble.
The headline is the only part of the entry that reaches the AI skill, so it has
to carry enough to act on. Spells out `joined(separator:)` rather than `joined`
for that reason: bare `joined()` is deliberately not rewritten, because on a lazy
sequence it resolves to the sequence-flattening overload and silently turns a
`String` into a lazy sequence. A headline saying just `joined` would be read as
license to rewrite it.
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md
Comment thread README.md

This applies to any operation that turns the sequence into a single value: `joined(separator:)`, `min`, `max`, `reduce`, and also `contains`, `allSatisfy`, and `first(where:)`, which stop as soon as they have an answer. It does not apply to an operation that produces another sequence, like `filter` or `sorted()`, since the transformed elements are needed more than once — or, in `sorted()`'s case, have to be materialized anyway.

When the operation takes a predicate, folding the transform into that predicate is better still, because then there is no `map` to make lazy:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea to mention this 👍🏻

@calda calda left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it thanks!

@calda calda left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

The `contains` example was a poor one to lead with: it is better written
`rows.contains(where: { $0.title.isEmpty })`, with no `map` at all. That holds for
every short-circuiting operation, since `xs.map(f).contains(where: p)` always
collapses to `xs.contains { p(f($0)) }` — so those make weak exemplars, because
adding `.lazy` is not the best available fix.

Leads with `joined(separator:)` and `min` instead, the two cases where there is no
better form: `joined` needs a sequence of strings, and the alternative to
`map { $0.y }.min()` is `min { $0.y < $1.y }?.y`, which is the shape rejected
during review of the rule itself.

Keeps the short-circuiting operations in the list, since the rule does improve
those chains, but adds a second example showing that folding the transform into
the predicate beats making the map lazy.

Also rewords the headline: "consumed in a single pass" was doing too much work.
"Reduces to a single result" says the same thing in plainer terms and explains
why `filter` is excluded — it produces another sequence rather than a value.
@jparise
jparise merged commit 44685a4 into master Aug 21, 2026
6 checks passed
@jparise
jparise deleted the prefer-lazy-map branch August 21, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants