View Source Pipe Chains

Pipe Start

  • If Credo.Check.Readability.BlockPipe is enabled, Quokka will prevent using blocks with pipes. Quokka respects the :exclude Credo opt.
  • If Credo.Check.Refactor.PipeChainStart is enabled, Quokka will rewrite the start of a pipechain to be a 0-arity function, a raw value, or a variable. Quokka respects the :excluded_functions and excluded_argument_types Credo opts.

Based on the Credo config, Quokka will rewrite the start of a pipechain to be a 0-arity function, a raw value, or a variable.

Enum.at(enum, 5)
|> IO.inspect()

# Styled:
enum
|> Enum.at(5)
|> IO.inspect()

If the start of a pipe is a block expression, Quokka will create a new variable to store the result of that expression and make that variable the start of the pipe.

if a do
  b
else
  c
end
|> Enum.at(4)
|> IO.inspect()

# Styled:
if_result =
  if a do
    b
  else
    c
  end

if_result
|> Enum.at(4)
|> IO.inspect()

Add parenthesis to function calls in pipes

This addresses Credo.Check.Readability.OneArityFunctionInPipe. This is not configurable.

a |> b |> c |> d
# Styled:
a |> b() |> c() |> d()

Remove Unnecessary then/2

When the piped argument is being passed as the first argument to the inner function, there's no need for then/2.

a |> then(&f(&1, ...)) |> b()
# Styled:
a |> f(...) |> b()
  • add parens to function calls |> fun |> => |> fun() |>

Add then/2 when defining and calling anonymous functions in pipes

a |> (fn x -> x end).() |> c()
# Styled:
a |> then(fn x -> x end) |> c()

Piped function optimizations

Two function calls into one! Fewer steps is always nice.

# reverse |> concat => reverse/2
a |> Enum.reverse() |> Enum.concat(enum) |> ...
# Styled:
a |> Enum.reverse(enum) |> ...

# filter |> count => count(filter)
a |> Enum.filter(filterer) |> Enum.count() |> ...
# Styled:
a |> Enum.count(filterer) |> ...

# map |> join => map_join
a |> Enum.map(mapper) |> Enum.join(joiner) |> ...
# Styled:
a |> Enum.map_join(joiner, mapper) |> ...

# Enum.map |> X.new() => X.new(mapper)
# where X is one of: Map, MapSet, Keyword
a |> Enum.map(mapper) |> Map.new() |> ...
# Styled:
a |> Map.new(mapper) |> ...

# Enum.map |> Enum.into(empty_collectable) => X.new(mapper)
# Where empty_collectable is one of `%{}`, `Map.new()`, `Keyword.new()`, `MapSet.new()`
# Given:
a |> Enum.map(mapper) |> Enum.into(%{}) |> ...
# Styled:
a |> Map.new(mapper) |> ...

# Given:
a |> b() |> Stream.each(fun) |> Stream.run()
a |> b() |> Stream.map(fun) |> Stream.run()
# Styled:
a |> b() |> Enum.each(fun)
a |> b() |> Enum.each(fun)

# Given:
a |> Enum.filter(fun) |> List.first() |> ...
a |> Enum.filter(fun) |> List.first(default) |> ...
# Styled:
a |> Enum.find(fun) |> ...
a |> Enum.find(default, fun) |> ...

# Consecutive Map.delete => Map.drop
a |> Map.delete(key1) |> Map.delete(key2) |> ...
# Styled:
a |> Map.drop([key1, key2]) |> ...

# Consecutive Map.drop => single Map.drop
a |> Map.drop([key1, key2]) |> Map.drop([key3, key4]) |> ...
# Styled:
a |> Map.drop([key1, key2, key3, key4]) |> ...

# Mixed Map.delete and Map.drop => single Map.drop
a |> Map.delete(key1) |> Map.drop([key2, key3]) |> ...
a |> Map.drop([key1, key2]) |> Map.delete(key3) |> ...
# Styled:
a |> Map.drop([key1, key2, key3]) |> ...
a |> Map.drop([key1, key2, key3]) |> ...

# Consecutive Keyword.delete => Keyword.drop
a |> Keyword.delete(key1) |> Keyword.delete(key2) |> ...
# Styled:
a |> Keyword.drop([key1, key2]) |> ...

# Consecutive Keyword.drop => single Keyword.drop
a |> Keyword.drop([key1, key2]) |> Keyword.drop([key3, key4]) |> ...
# Styled:
a |> Keyword.drop([key1, key2, key3, key4]) |> ...

# Mixed Keyword.delete and Keyword.drop => single Keyword.drop
a |> Keyword.delete(key1) |> Keyword.drop([key2, key3]) |> ...
a |> Keyword.drop([key1, key2]) |> Keyword.delete(key3) |> ...
# Styled:
a |> Keyword.drop([key1, key2, key3]) |> ...
a |> Keyword.drop([key1, key2, key3]) |> ...

# If [`Credo.Check.Refactor.UtcNowTruncate`](https://hexdocs.pm/credo/Credo.Check.Refactor.UtcNowTruncate.html)
# is enabled, rewrites (DateTime|NaiveDateTime).utc_now() |> (DateTime|NaiveDateTime).truncate(precision)
DateTime.utc_now() |> DateTime.truncate(:millisecond)
NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
# Styled:
DateTime.utc_now(:millisecond)
NaiveDateTime.utc_now(:second)

Pipe into Case

When a case statement has a pipe chain as its subject, Quokka rewrites it to use the |> case do syntax.

case foo
     |> bar()
     |> baz() do
  x -> x
end

# Styled:
foo
|> bar()
|> baz()
|> case do
  x -> x
end

This rewrite can be disabled by adding :pipe_into_case to the exclude list in your .formatter.exs:

[
  plugins: [Quokka],
  quokka: [
    exclude: [:pipe_into_case]
  ]
]

Unpiping Single Pipes

This addresses Credo.Check.Readability.SinglePipe. If the Credo check is enabled, Quokka will rewrite pipechains with a single pipe to be function calls. Notably, this rule combined with the optimizations rewrites above means some chains with more than one pipe will also become function calls.

foo = bar |> baz()
# Styled:
foo = baz(bar)

map = a |> Enum.map(mapper) |> Map.new()
# Styled:
map = Map.new(a, mapper)

Excluding Functions from Pipe Rewrites

You can exclude specific functions from pipe-related rewrites using the exclude: [piped_functions: [...]] option in your .formatter.exs. This affects:

  1. Pipe nesting: Functions won't be moved into pipe chains
  2. SinglePipe: Single pipes ending with excluded functions won't be collapsed
  3. PipeChainStart: Excluded functions at the start of pipes won't have their arguments extracted
# .formatter.exs
[
  plugins: [Quokka],
  quokka: [
    exclude: [piped_functions: [:from, :"Repo.insert"]]
  ]
]

With this configuration:

# SinglePipe: won't collapse because Repo.insert is excluded
changeset
|> Repo.insert()

# PipeChainStart: won't extract because Repo.insert is excluded
Repo.insert(changeset)
|> Ecto.Multi.run(:something, fn _, _ -> :ok end)
|> Repo.transaction()

# from is excluded, so this stays as-is
from(f in Foo, where: f.id == ^id)
|> Repo.all()