View Source
Control Flow Macros (case, if, unless, cond, with)
Elixir's Kernel documentation refers to these structures as "macros for control-flow". We often refer to them as "blocks" in our changelog, which is a much worse name, to be sure.
if and unless
Quokka removes else: nil clauses:
if a, do: b, else: nil
# styled:
if a, do: bQuokka removes unless since it is being deprecated in Elixir 1.18. This implicitly addresses Credo.Check.Refactor.NegatedConditionsInUnless and Credo.Check.Refactor.NegatedConditionsWithElse.
# Given:
unless a, do: b
# Styled:
if a, do: bNegation Inversion
This addresses Credo.Check.Refactor.NegatedConditionsWithElse. This is not configurable.
Quokka removes negators in the head of if statements by "inverting" the statement.
The following operators are considered "negators": !, not, !=, !==
Examples:
# negated `if` statements with an `else` clause have their clauses inverted and negation removed
if !x, do: y, else: z
# Styled:
if x, do: z, else: y
# negated `unless` statements are rewritten to `if`
unless x != y, do: z
# B styled:
if x == y, do: z
# `unless` with `else` is verboten; these are always rewritten to `if` statements
unless x, do: y, else: z
# styled:
if x, do: z, else: yBecause elixir relies on truthy/falsey values for its if statements, boolean casting is unnecessary and so double negation is simply removed.
if !!x, do: y
# styled:
if x, do: y
cond
This addresses Credo.Check.Refactor.CondStatements. This is not configurable.
Quokka has only one cond statement rewrite: replace 2-clause statements with if statements.
# Given
cond do
a -> b
true -> c
end
# Styled
if a do
b
else
c
end
with
This addresses Credo.Check.Readability.WithSingleClause, Credo.Check.Refactor.RedundantWithClauseResult, and Credo.Check.Refactor.WithClauses. This is not configurable.
Remove Identity Else Clause
Like if statements with nil as their else clause, the identity else clause is the default for with statements and so is removed.
# Given
with :ok <- b(), :ok <- b() do
foo()
else
error -> error
end
# Styled:
with :ok <- b(), :ok <- b() do
foo()
endRemove The Statement Entirely
While you might think "surely this kind of code never appears in the wild", it absolutely does. Typically it's the result of someone refactoring a pattern away and not looking at the larger picture and realizing that the with statement now serves no purpose.
Maybe someday the compiler will warn about these use cases. Until then, Quokka to the rescue.
# Given:
with a <- b(),
c <- d(),
e <- f(),
do: g,
else: (_ -> h)
# Styled:
a = b()
c = d()
e = f()
g
# Given
with value <- arg do
value
end
# Styled:
arg
Replace _ <- rhs with rhs
This is another case of "less is more" for the reader.
# Given
with :ok <- x,
_ <- y(),
{:ok, _} <- z do
:ok
end
# Styled:
with :ok <- x,
y(),
{:ok, _} <- z do
:ok
end
Replace non-branching bar <- with bar =
<- is for branching. If the lefthand side is the trivial match (a bare variable), Quokka rewrites it to use the = operator instead.
# Given
with :ok <- foo(),
bar <- baz(),
:ok <- woo(),
do: {:ok, bar}
# Styled
with :ok <- foo(),
bar = baz(),
:ok <- woo(),
do: {:ok, bar}
Move assignments from with statement head
Just because any program could be written entirely within the head of a with statement doesn't mean it should be!
Quokka moves assignments that aren't trapped between <- outside of the head. Combined with the non-pattern-matching replacement above, we get the following:
# Given
with foo <- bar,
x = y,
:ok <- baz,
bop <- boop,
:ok <- blop,
foo <- bar,
:success = hope_this_works! do
:ok
end
# Styled:
foo = bar
x = y
with :ok <- baz,
bop = boop,
:ok <- blop do
foo = bar
:success = hope_this_works!
:ok
endRemove redundant final clause
If the pattern of the final clause of the head is also the with statements do body, quokka nixes the final match and makes the right hand side of the clause into the do body.
# Given
with {:ok, a} <- foo(),
{:ok, b} <- bar(a) do
{:ok, b}
end
# Styled:
with {:ok, a} <- foo() do
bar(a)
end
Replace with case
A with statement with a single clause in the head and an else body is really just a case statement putting on airs.
# Given:
with :ok <- foo do
:success
else
:fail -> :failure
error -> error
end
# Styled:
case foo do
:ok -> :success
:fail -> :failure
error -> error
end
Replace with if
Given Quokka rewrites trivial case to if, it shouldn't be a surprise that that same rule means that with can be rewritten to if in some cases.
# Given:
with true <- foo(), bar <- baz() do
{:ok, bar}
else
_ -> :error
end
# Styled:
if foo() do
bar = baz()
{:ok, bar}
else
:error
end