Credence.Pattern.NoReduceForGroupBy
(credence v0.7.0)
Copy Markdown
Detects a manual Enum.group_by/2 written as Enum.reduce/3 with an
empty-map accumulator that prepends each element onto a per-key list via
Map.update(acc, key, [elem], &[elem | &1]), immediately followed by
|> Map.new(fn {k, v} -> {k, Enum.reverse(v)} end) to restore insertion
order.
Only this complete pipeline is flagged, because only this complete pipeline
is exactly equivalent to Enum.group_by/2 on every input.
Why the full pipeline is required
The bare reduce builds each value list in reverse insertion order (prepend
with [elem | &1]):
Enum.reduce(["a1", "a2"], %{}, fn x, acc ->
Map.update(acc, String.first(x), [x], &[x | &1])
end)
#=> %{"a" => ["a2", "a1"]}whereas Enum.group_by/2 keeps insertion order (%{"a" => ["a1", "a2"]}).
The trailing Map.new(fn {k, v} -> {k, Enum.reverse(v)} end) reverses each
value list, making the whole expression equal to:
Enum.group_by(enum, fn x -> String.first(x) end)A bare reduce without the reverse is therefore not flagged: it produces a different (reverse-order) result, so there is no behaviour-preserving fix.
Flagged pattern (auto-fixed)
Enum.reduce(enum, %{}, fn x, acc ->
Map.update(acc, key(x), [x], &[x | &1])
end)
|> Map.new(fn {k, v} -> {k, Enum.reverse(v)} end)becomes
Enum.group_by(enum, fn x -> key(x) end)The reduce may be written directly (Enum.reduce(enum, %{}, fn ...)) or piped
(enum |> Enum.reduce(%{}, fn ...)). The key may be computed inline in the
Map.update call, or via a single key = ... binding immediately preceding
the Map.update (the only other statement in the function body).