Skip to content

Latest commit

 

History

History
250 lines (198 loc) · 5.54 KB

control_flow_macros.md

File metadata and controls

250 lines (198 loc) · 5.54 KB

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: b

Quokka 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: b

Negation 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: y

Because 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()
end

Remove 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
end

Remove 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