Elixir v1.1.0 Inspect.Algebra

A 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 String nodes, and a custom rendering function that maximises horizontal space use.

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 break/2, which converts the given string into a line break depending on how much space there is to print. Let’s glue two docs together with a break and then render it:

iex> doc = Inspect.Algebra.glue("a", " ", "b")
iex> Inspect.Algebra.format(doc, 80)
["a", " ", "b"]

Notice 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> Inspect.Algebra.format(doc, 10)
["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]

Finally, this module also contains Elixir related functions, a bit tied to Elixir formatting, namely surround/3 and surround_many/5.

Implementation details

The original Haskell implementation of the algorithm by Wadler relies on lazy evaluation to unfold document groups on two alternatives: :flat (breaks as spaces) and :break (breaks as newlines). Implementing the same logic in a strict language such as Elixir leads to an exponential growth of possible documents, unless document groups are encoded explictly as :flat or :break. Those groups are then reduced to a simple document, where the layout is already decided, per Lindig.

This implementation slightly changes the semantic of Lindig’s algorithm to allow elements that belong to the same group to be printed together in the same line, even if they do not fit the line fully. This was achieved by changing :break to mean a possible break and :flat to force a flat structure. Then deciding if a break works as a newline is just a matter of checking if we have enough space until the next break that is not inside a group (which is still flat).

Custom pretty printers can be implemented using the documents returned by this module and by providing their own rendering functions.

Summary

Functions

Document entity representing a break

Concatenates a list of documents

Concatenates two document entities

Returns a document entity used to represent nothingness

Folds a list of document entities into a document entity using a function that is passed as the first argument

The formatting function

Inserts a break between two docs. See break/1 for more info

Inserts a break, passed as the second argument, between two docs, the first and the third arguments

Returns a group containing the specified document

Inserts a mandatory linebreak between two document entities

Nests document entity x positions deep

Inserts a mandatory single space between two document entities

Surrounds a document with characters

Converts an Elixir structure to an algebra document according to the inspect protocol

Types

t ::
  :doc_nil |
  :doc_line |
  doc_cons |
  doc_nest |
  doc_break |
  doc_group |
  binary

Functions

break()

Specs

break :: doc_break
break(s)

Specs

break(binary) :: doc_break

Document entity representing a break.

This break can be rendered as a linebreak or as spaces, depending on the mode of the chosen layout or the provided separator.

Examples

Let’s glue two docs together with a break and then render it:

iex> doc = Inspect.Algebra.glue("a", " ", "b")
iex> Inspect.Algebra.format(doc, 80)
["a", " ", "b"]

Notice 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> Inspect.Algebra.format(doc, 10)
["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]
concat(docs)

Specs

concat([t]) :: doc_cons

Concatenates a list of documents.

concat(x, y)

Specs

concat(t, t) :: doc_cons

Concatenates two document entities.

Examples

iex> doc = Inspect.Algebra.concat "hello", "world"
iex> Inspect.Algebra.format(doc, 80)
["hello", "world"]
empty()

Specs

empty :: :doc_nil

Returns a document entity used to represent nothingness.

Examples

iex> Inspect.Algebra.empty
:doc_nil
fold_doc(list, fun)

Specs

fold_doc([t], (t, t -> t)) :: t

Folds a list of document entities into a document entity using a function that is passed as the first argument.

Examples

iex> doc = ["A", "B"]
iex> doc = Inspect.Algebra.fold_doc(doc, fn(x, y) ->
...>   Inspect.Algebra.concat [x, "!", y]
...> end)
iex> Inspect.Algebra.format(doc, 80)
["A", "!", "B"]
format(d, w)

Specs

format(t, non_neg_integer | :infinity) :: iodata

The formatting function.

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.

glue(x, y)

Specs

glue(t, t) :: doc_cons

Inserts a break between two docs. See break/1 for more info.

glue(x, g, y)

Specs

glue(t, binary, t) :: doc_cons

Inserts a break, passed as the second argument, between two docs, the first and the third arguments.

group(d)

Specs

group(t) :: doc_group

Returns a group containing the specified document.

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", " ", "B"]
line(x, y)

Specs

line(t, t) :: doc_cons

Inserts a mandatory linebreak between two document entities.

Examples

iex> doc = Inspect.Algebra.line "Hughes", "Wadler"
iex> Inspect.Algebra.format(doc, 80)
["Hughes", "\n", "Wadler"]
nest(x, i)

Specs

nest(t, non_neg_integer) :: doc_nest

Nests document entity x positions deep.

Nesting will be appended to the line breaks.

Examples

iex> doc = Inspect.Algebra.nest(Inspect.Algebra.glue("hello", "world"), 5)
iex> Inspect.Algebra.format(doc, 5)
["hello", "\n     ", "world"]
space(x, y)

Specs

space(t, t) :: doc_cons

Inserts a mandatory single space between two document entities.

Examples

iex> doc = Inspect.Algebra.space "Hughes", "Wadler"
iex> Inspect.Algebra.format(doc, 80)
["Hughes", " ", "Wadler"]
surround(left, doc, right)

Specs

surround(binary, t, binary) :: t

Surrounds a document with characters.

Puts the document between left and right enclosing and nesting it. The document is marked as a group, to show the maximum as possible concisely together.

Examples

iex> doc = Inspect.Algebra.surround "[", Inspect.Algebra.glue("a", "b"), "]"
iex> Inspect.Algebra.format(doc, 3)
["[", "a", "\n ", "b", "]"]
surround_many(left, docs, right, opts, fun, separator \\ ",")

Specs

surround_many(binary, [any], binary, Inspect.Opts.t, (term, Inspect.Opts.t -> t), binary) :: t

Maps and glues a collection of items.

It uses the given left and right as surrounding and a separator for each item. A limit can be passed which, once reached, stops gluing and outputs “…” instead.

Examples

iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]",
...>         %Inspect.Opts{limit: :infinity}, fn i, _opts -> to_string(i) end)
iex> Inspect.Algebra.format(doc, 5) |> IO.iodata_to_binary
"[1,\n 2,\n 3,\n 4,\n 5]"

iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]",
...>         %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end)
iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
"[1, 2, 3, ...]"

iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]",
...>         %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end, "!")
iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
"[1! 2! 3! ...]"
to_doc(map, opts)

Specs

to_doc(any, Inspect.Opts.t) :: t

Converts an Elixir structure to an algebra document according to the inspect protocol.