Credence.Pattern.NoMapThenAggregate (credence v0.7.1)

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).