Credence.Pattern.NoMapThenAggregate
(credence v0.7.0)
Copy Markdown
Detects Enum.map/2 immediately followed by Enum.sum/1, which creates an
unnecessary intermediate list, and fuses it into a single Enum.reduce/3.
Why this matters
LLMs default to "transform then aggregate" as the natural functional
decomposition. While readable, the intermediate list from Enum.map
is allocated only to be traversed once and discarded:
# Flagged — two passes, intermediate list allocation
numbers
|> Enum.map(&Enum.sum/1)
|> Enum.sum()
# Better — single pass, no intermediate list
numbers
|> Enum.reduce(0, fn x, acc -> acc + Enum.sum(x) end)Only sum (aggregation), not max/min (selection)
Only Enum.sum/1 is fused. + has an identity (0) that seeds the reduce,
and the mapper is applied to every element. Enum.max/1/Enum.min/1 are
not flagged: the natural fusion is Enum.reduce/2 (no init), whose seed is
the first, unmapped element — so [5] |> Enum.map(f) |> Enum.max() would
give f.(5) but the fused reduce/2 gives the raw 5. There is no identity
to seed max/min, so the mapper can't be applied to the seed — the fusion is not
behaviour-preserving. (Selection-vs-aggregation: same reason no_explicit_max_reduce
was dropped.)
Flagged patterns
Enum.map(f) piped into or wrapping Enum.sum/1 (pipeline and direct-call forms).