Inspect.Algebra (Elixir v1.19.0-dev)
View SourceA set of functions for creating and manipulating algebra documents.
This module implements the functionality described in "Strictly Pretty" (2000) by Christian Lindig with small additions, like support for binary nodes and a break mode that maximises use of horizontal space.
iex> Inspect.Algebra.empty()
:doc_nil
iex> "foo"
"foo"
With the functions in this module, we can concatenate different elements together and render them:
iex> doc = Inspect.Algebra.concat(Inspect.Algebra.empty(), "foo")
iex> Inspect.Algebra.format(doc, 80)
["foo"]
The functions nest/2
, space/2
and line/2
help you put the
document together into a rigid structure. However, the document
algebra gets interesting when using functions like glue/3
and
group/1
. A glue inserts a break between two documents. A group
indicates a document that must fit the current line, otherwise
breaks are rendered as new lines. Let's glue two docs together
with a break, group it and then render it:
iex> doc = Inspect.Algebra.glue("a", " ", "b")
iex> doc = Inspect.Algebra.group(doc)
iex> Inspect.Algebra.format(doc, 80)
["a", " ", "b"]
Note that the break was represented as is, because we haven't reached a line limit. Once we do, it is replaced by a newline:
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
iex> doc = Inspect.Algebra.group(doc)
iex> Inspect.Algebra.format(doc, 10)
["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]
This module uses the byte size to compute how much space there is
left. If your document contains strings, then those need to be
wrapped in string/1
, which then relies on String.length/1
to
precompute the document size.
Finally, this module also contains Elixir related functions, a bit
tied to Elixir formatting, such as to_doc/2
.
Implementation details
The implementation of Inspect.Algebra
is based on the Strictly Pretty
paper by Lindig which builds on top of previous pretty printing
algorithms but is tailored to strict languages, such as Elixir.
The core idea in the paper is the use of explicit document groups which
are rendered as flat (breaks as spaces) or as break (breaks as newlines).
This implementation provides two types of breaks: :strict
and :flex
.
When a group does not fit, all strict breaks are treated as newlines.
Flex breaks, however, are re-evaluated on every occurrence and may still
be rendered flat. See break/1
and flex_break/1
for more information.
This implementation also adds force_unfit/1
and optimistic/pessimistic
groups which give more control over the document fitting.
Summary
Functions
Returns a break document based on the given string
.
Collapse any new lines and whitespace following this
node, emitting up to max
new lines.
Colors a document with the given color (preceding the document itself).
Colors a document if the color_key
has a color in the options.
Concatenates a list of documents returning a new document.
Concatenates two document entities returning a new document.
Wraps collection
in left
and right
according to limit and contents.
Returns a document entity used to represent nothingness.
Returns a flex break document based on the given string
.
Glues two documents (doc1
and doc2
) inserting a
flex_break/1
given by break_string
between them.
Folds a list of documents into a document using the given folder function.
Forces the current group to be unfit.
Formats a given document for a given width.
Glues two documents (doc1
and doc2
) inserting the given
break break_string
between them.
Returns a group containing the specified document doc
.
A mandatory linebreak.
Inserts a mandatory linebreak between two documents.
Nests the given document at the given level
.
Considers the next break as fit.
Disable any rendering limit while rendering the given document.
Inserts a mandatory single space between two documents.
Creates a document represented by string.
Converts an Elixir term to an algebra document
according to the Inspect
protocol.
Guards
Types
@type t() :: binary() | :doc_line | :doc_nil | doc_break() | doc_collapse() | doc_color() | doc_cons() | doc_fits() | doc_force() | doc_group() | doc_nest() | doc_string() | doc_limit()
Functions
@spec break(binary()) :: doc_break()
Returns a break document based on the given string
.
This break can be rendered as a linebreak or as the given string
,
depending on the mode
of the chosen layout.
Examples
Let's create a document by concatenating two strings with a break between them:
iex> doc = Inspect.Algebra.concat(["a", Inspect.Algebra.break("\t"), "b"])
iex> Inspect.Algebra.format(doc, 80)
["a", "\t", "b"]
Note that the break was represented with the given string, because we didn't reach a line limit. Once we do, it is replaced by a newline:
iex> break = Inspect.Algebra.break("\t")
iex> doc = Inspect.Algebra.concat([String.duplicate("a", 20), break, "b"])
iex> doc = Inspect.Algebra.group(doc)
iex> Inspect.Algebra.format(doc, 10)
["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]
@spec collapse_lines(pos_integer()) :: doc_collapse()
Collapse any new lines and whitespace following this
node, emitting up to max
new lines.
Colors a document with the given color (preceding the document itself).
@spec color_doc(t(), Inspect.Opts.color_key(), Inspect.Opts.t()) :: t()
Colors a document if the color_key
has a color in the options.
Concatenates a list of documents returning a new document.
Examples
iex> doc = Inspect.Algebra.concat(["a", "b", "c"])
iex> Inspect.Algebra.format(doc, 80)
["a", "b", "c"]
Concatenates two document entities returning a new document.
Examples
iex> doc = Inspect.Algebra.concat("hello", "world")
iex> Inspect.Algebra.format(doc, 80)
["hello", "world"]
@spec container_doc( t(), [term()], t(), Inspect.Opts.t(), (term(), Inspect.Opts.t() -> t()), keyword() ) :: t()
Wraps collection
in left
and right
according to limit and contents.
It uses the given left
and right
documents as surrounding and the
separator document separator
to separate items in docs
. If all entries
in the collection are simple documents (texts or strings), then this function
attempts to put as much as possible on the same line. If they are not simple,
only one entry is shown per line if they do not fit.
The limit in the given inspect_opts
is respected and when reached this
function stops processing and outputs "..."
instead.
Options
:separator
- the separator used between each doc:break
- If:strict
, always break between each element. If:flex
, breaks only when necessary. If:maybe
, chooses:flex
only if all elements are text-based, otherwise is:strict
Examples
iex> inspect_opts = %Inspect.Opts{limit: :infinity}
iex> fun = fn i, _opts -> to_string(i) end
iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]", inspect_opts, fun)
iex> Inspect.Algebra.format(doc, 5) |> IO.iodata_to_binary()
"[1,\n 2,\n 3,\n 4,\n 5]"
iex> inspect_opts = %Inspect.Opts{limit: 3}
iex> fun = fn i, _opts -> to_string(i) end
iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]", inspect_opts, fun)
iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary()
"[1, 2, 3, ...]"
iex> inspect_opts = %Inspect.Opts{limit: 3}
iex> fun = fn i, _opts -> to_string(i) end
iex> opts = [separator: "!"]
iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]", inspect_opts, fun, opts)
iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary()
"[1! 2! 3! ...]"
@spec empty() :: :doc_nil
Returns a document entity used to represent nothingness.
Examples
iex> Inspect.Algebra.empty()
:doc_nil
@spec flex_break(binary()) :: doc_break()
Returns a flex break document based on the given string
.
A flex break still causes a group to break, like break/1
,
but it is re-evaluated when the documented is rendered.
For example, take a group document represented as [1, 2, 3]
where the space after every comma is a break. When the document
above does not fit a single line, all breaks are enabled,
causing the document to be rendered as:
[1,
2,
3]
However, if flex breaks are used, then each break is re-evaluated when rendered, so the document could be possible rendered as:
[1, 2,
3]
Hence the name "flex". they are more flexible when it comes to the document fitting. On the other hand, they are more expensive since each break needs to be re-evaluated.
This function is used by container_doc/6
and friends to the
maximum number of entries on the same line.
Glues two documents (doc1
and doc2
) inserting a
flex_break/1
given by break_string
between them.
This function is used by container_doc/6
and friends
to the maximum number of entries on the same line.
Folds a list of documents into a document using the given folder function.
The list of documents is folded "from the right"; in that, this function is
similar to List.foldr/3
, except that it doesn't expect an initial
accumulator and uses the last element of docs
as the initial accumulator.
Examples
iex> docs = ["A", "B", "C"]
iex> docs =
...> Inspect.Algebra.fold(docs, fn doc, acc ->
...> Inspect.Algebra.concat([doc, "!", acc])
...> end)
iex> Inspect.Algebra.format(docs, 80)
["A", "!", "B", "!", "C"]
@spec force_unfit(t()) :: doc_force()
Forces the current group to be unfit.
@spec format(t(), non_neg_integer() | :infinity) :: iodata()
Formats a given document for a given width.
Takes the maximum width and a document to print as its arguments and returns an IO data representation of the best layout for the document to fit in the given width.
The document starts flat (without breaks) until a group is found.
Examples
iex> doc = Inspect.Algebra.glue("hello", " ", "world")
iex> doc = Inspect.Algebra.group(doc)
iex> doc |> Inspect.Algebra.format(30) |> IO.iodata_to_binary()
"hello world"
iex> doc |> Inspect.Algebra.format(10) |> IO.iodata_to_binary()
"hello\nworld"
Glues two documents (doc1
and doc2
) inserting the given
break break_string
between them.
For more information on how the break is inserted, see break/1
.
Examples
iex> doc = Inspect.Algebra.glue("hello", "world")
iex> Inspect.Algebra.format(doc, 80)
["hello", " ", "world"]
iex> doc = Inspect.Algebra.glue("hello", "\t", "world")
iex> Inspect.Algebra.format(doc, 80)
["hello", "\t", "world"]
@spec group(t(), :normal | :optimistic | :pessimistic) :: doc_group()
Returns a group containing the specified document doc
.
Documents in a group are attempted to be rendered together
to the best of the renderer ability. If there are break/1
s
in the group and the group does not fit the given width,
the breaks are converted into lines. Otherwise the breaks
are rendered as text based on their string contents.
There are three types of groups, described next.
Group modes
:normal
- the group fits if it fits within the given width:optimistic
- the group fits if it fits within the given width. However, when nested within another group, the parent group will assume this group fits as long as it has a single break, even if the optimistic group has aforce_unfit/1
document within it. Overall, this has an effect similar to swapping the order groups break. For example, if you have aparent_group(child_group)
and they do not fit, the parent converts breaks into newlines first, allowing the child to compute if it fits. However, if the child group is optimistic and it has breaks, then the parent assumes it fits, leaving the overall fitting decision to the child:pessimistic
- the group fits if it fits within the given width. However it disables any optimistic group within it
Examples
iex> doc =
...> Inspect.Algebra.group(
...> Inspect.Algebra.concat(
...> Inspect.Algebra.group(
...> Inspect.Algebra.concat(
...> "Hello,",
...> Inspect.Algebra.concat(
...> Inspect.Algebra.break(),
...> "A"
...> )
...> )
...> ),
...> Inspect.Algebra.concat(
...> Inspect.Algebra.break(),
...> "B"
...> )
...> )
...> )
iex> Inspect.Algebra.format(doc, 80)
["Hello,", " ", "A", " ", "B"]
iex> Inspect.Algebra.format(doc, 6)
["Hello,", "\n", "A", "\n", "B"]
Mode examples
The different groups modes are used by Elixir's code formatter to avoid breaking code at some specific locations. For example, consider this code:
some_function_call(%{..., key: value, ...})
Now imagine that this code does not fit its line. The code
formatter introduces breaks inside (
and )
and inside
%{
and }
, each within their own group. Therefore the
document would break as:
some_function_call(
%{
...,
key: value,
...
}
)
To address this, the formatter marks the inner group as optimistic.
This means the first group, which is (...)
will consider the document
fits and avoids adding breaks around the parens. So overall the code
is formatted as:
some_function_call(%{
...,
key: value,
...
})
@spec line() :: t()
A mandatory linebreak.
A group with linebreaks will fit if all lines in the group fit.
Examples
iex> doc =
...> Inspect.Algebra.concat(
...> Inspect.Algebra.concat(
...> "Hughes",
...> Inspect.Algebra.line()
...> ),
...> "Wadler"
...> )
iex> Inspect.Algebra.format(doc, 80)
["Hughes", "\n", "Wadler"]
Inserts a mandatory linebreak between two documents.
See line/0
.
Examples
iex> doc = Inspect.Algebra.line("Hughes", "Wadler")
iex> Inspect.Algebra.format(doc, 80)
["Hughes", "\n", "Wadler"]
@spec nest(t(), non_neg_integer() | :cursor | :reset, :always | :break) :: doc_nest() | t()
Nests the given document at the given level
.
If level
is an integer, that's the indentation appended
to line breaks whenever they occur. If the level is :cursor
,
the current position of the "cursor" in the document becomes
the nesting. If the level is :reset
, it is set back to 0.
mode
can be :always
, which means nesting always happen,
or :break
, which means nesting only happens inside a group
that has been broken.
Examples
iex> doc = Inspect.Algebra.nest(Inspect.Algebra.glue("hello", "world"), 5)
iex> doc = Inspect.Algebra.group(doc)
iex> Inspect.Algebra.format(doc, 5)
["hello", "\n ", "world"]
@spec next_break_fits(t(), :enabled | :disabled) :: doc_fits()
Considers the next break as fit.
Disable any rendering limit while rendering the given document.
Examples
iex> doc = Inspect.Algebra.glue("hello", "world") |> Inspect.Algebra.group()
iex> Inspect.Algebra.format(doc, 10)
["hello", "\n", "world"]
iex> doc = Inspect.Algebra.no_limit(doc)
iex> Inspect.Algebra.format(doc, 10)
["hello", " ", "world"]
Inserts a mandatory single space between two documents.
Examples
iex> doc = Inspect.Algebra.space("Hughes", "Wadler")
iex> Inspect.Algebra.format(doc, 5)
["Hughes", " ", "Wadler"]
@spec string(String.t()) :: doc_string()
Creates a document represented by string.
While Inspect.Algebra
accepts binaries as documents,
those are counted by binary size. On the other hand,
string
documents are measured in terms of graphemes
towards the document size.
Examples
The following document has 10 bytes and therefore it does not format to width 9 without breaks:
iex> doc = Inspect.Algebra.glue("olá", " ", "mundo")
iex> doc = Inspect.Algebra.group(doc)
iex> Inspect.Algebra.format(doc, 9)
["olá", "\n", "mundo"]
However, if we use string
, then the string length is
used, instead of byte size, correctly fitting:
iex> string = Inspect.Algebra.string("olá")
iex> doc = Inspect.Algebra.glue(string, " ", "mundo")
iex> doc = Inspect.Algebra.group(doc)
iex> Inspect.Algebra.format(doc, 9)
["olá", " ", "mundo"]
@spec to_doc(any(), Inspect.Opts.t()) :: t()
Converts an Elixir term to an algebra document
according to the Inspect
protocol.