Credence.Pattern.NoHdTlWhenConsBound
(credence v0.7.0)
Copy Markdown
Detects hd(var) / tl(var) calls in a function body where var is
already bound to a non-empty list by an anonymous cons pattern
(var = [_ | _] or [_ | _] = var) in the function clause head, and
rewrites them to destructure [head | tail] in the head, using head /
tail directly instead of Kernel.hd/1 / Kernel.tl/1.
Because the head pattern already guarantees var matched [_ | _] (a
non-empty, possibly improper list), hd(var) is exactly the head and
tl(var) is exactly the tail of a [head | tail] = var destructure —
for every admitted value, including improper lists like [1 | 2]. The
rewrite is therefore answer-preserving.
Narrowing — the safe core
The fix only fires when it is mechanically guaranteed safe:
Anonymous cons only. Only
[_ | _](both sides the bare_) is handled. A named cons ([h | t],[_head | _tail]) already binds the parts and is left alone — promoting those would mean reusing or renaming existing (possibly underscore-suppressed) bindings.- No rebinding in the body. The body must contain no binding/scoping
form (
=,fn,for,with,case,cond,receive,try,quote). This rules out any inner scope that could shadowvar, so a matchedhd(var)/tl(var)unambiguously refers to the head binding. - Keep the variable when it is still needed. If
varis referenced anywhere outside the rewrittenhd/tlcalls (a guard, another parameter, or elsewhere in the body), thevar = ...binding is kept (var = [head | tail]); otherwise the whole binding collapses to the cons pattern ([head | tail]).
Bad
def first(list = [_ | _]), do: hd(list)
def split(list = [_ | _]), do: {hd(list), tl(list)}Good
def first([head | _]), do: head
def split([head | tail]), do: {head, tail}