Text · Lesson 14.11

Render

Source
Format into a String you keep with Render, which returns String ! FormatError and refuses a wrong pattern before writing anything.

PrintLine formats text and sends it straight to the console. Often you want the formatted text itself — to keep it, measure it, pass it on, or print it later. Render, from the Format package, does the same formatting but hands the text back as a String:

Render(allocator, pattern, values...) -> String ! FormatError

Its result is fallible, and the way it fails is the other half of this lesson: a wrong pattern is found before anything is written, and the error says exactly what was wrong.

Text you keep

InvoiceCode builds a code such as INV-0042 once, as a value the caller owns:

func InvoiceCode(allocator: Allocator, number: int) -> String ! FormatError {
    return Render(allocator, "INV-{:04}", number);
}

The pattern is the same as PrintLine's — {:04} is the zero padding from Format number. Each call produces a String that lives on after it, unlike a printed line, so Main can print both codes in one line and measure one of them:

let first = InvoiceCode(allocator, 7)?;
let second = InvoiceCode(allocator, 42)?;
PrintLine("codes     {} and {}, {} bytes each", first, second, first.Length());

Why it can fail

Two kinds of thing can go wrong. The String needs memory, and the pattern itself can be wrong. FormatError is a variant with a case for each:

CaseWhen
InvalidSpecification(at)a spec does not parse; at is the byte where it stopped
UnsupportedRequesta spec parses, but asks for something this value does not support
ArgumentCountMismatch(wanted, given)placeholders and values do not match in number
InsufficientCapacitya destination of fixed size cannot take the whole text
ValueOutOfRangea valid value cannot be shown in the form asked for
TextFailure(kind)text handling failed — this is where running out of memory arrives
WriterFailurethe destination failed on its own terms

Show takes an outcome apart with two nested matches. The cases that carry a position or counts are taken apart; everything else prints its own description through else:

func Show(outcome: String ! FormatError) {
    match outcome {
        .Success(text) => PrintLine("rendered  {}", text),
        .Failure(error) => match error {
            .InvalidSpecification(at) => PrintLine("refused   bad spec at byte {}", at),
            .ArgumentCountMismatch(wanted, given) =>
                PrintLine("refused   {} placeholders but {} values", wanted, given),
            else => PrintLine("refused   {}", error)
        }
    }
}

One right pattern and three wrong ones

Show(Render(allocator, "{} of {}", 3, 10));
Show(Render(allocator, "[{:>>>}]", 3));
let pattern = "{}, {} and {}";
Show(Render(allocator, pattern, 3, 4));
Show(Render(allocator, "{:x}", "text"));
PatternValuesResult
"{} of {}"3, 103 of 10
"[{:>>>}]"3InvalidSpecification(5) — >> is a fill and an alignment; the third > fits nowhere
pattern3, 4ArgumentCountMismatch(3, 2)
"{:x}""text"UnsupportedRequest — text has no hexadecimal
flowchart LR
    r["Render(allocator, pattern, values…)"] --> m{"Memory for<br/>the String?"}
    m -- "no" --> f2["Failure:<br/>TextFailure(OutOfMemory)"]
    m -- "yes" --> c{"Is the pattern right<br/>for these values?"}
    c -- "no" --> f["Failure: the FormatError case;<br/>no half rendering is returned"]
    c -- "yes" --> s["Success: the String"]

Counted while compiling, or while running

Why is the third pattern in a variable? Because the compiler counts the placeholders of a pattern written in the call itself. Render(allocator, "{}, {} and {}", 3, 4) does not build at all. A pattern that arrives as a value — read from a file, chosen at run time — can be counted only when it runs, and that is when ArgumentCountMismatch appears.

PrintLine refuses the same patterns too, but its IoError can only say that the request was invalid, not where or why. When a pattern might be wrong, Render is the one that tells you.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// `PrintLine` formats text and sends it straight to the console. `Render`, from the Format
// package, does the same formatting but hands the text back as a `String`, to be kept, measured,
// passed on or printed later:
//
//     Render(allocator, pattern, values...) -> String ! FormatError
//
// The result is fallible because two things can go wrong. The String needs memory, and the
// pattern itself can be wrong: a spec that does not parse, a spec the value cannot honour, or
// a different number of placeholders than values. A wrong pattern is found before anything is
// written, so a failure never leaves half a rendering behind.
import Allocator::{ Allocator, SystemAllocator };
import Format::Render;
import Io::PrintLine;
import Text::{ FormatError, String };

// Builds a code such as "INV-0042" once, as a value the caller owns.
func InvoiceCode(allocator: Allocator, number: int) -> String ! FormatError {
    return Render(allocator, "INV-{:04}", number);
}

// Reports either outcome of a render. `FormatError` is a variant, so the cases that carry a
// position or counts can be taken apart; the rest print their own description.
func Show(outcome: String ! FormatError) {
    match outcome {
        .Success(text) => PrintLine("rendered  {}", text),
        .Failure(error) => match error {
            .InvalidSpecification(at) => PrintLine("refused   bad spec at byte {}", at),
            .ArgumentCountMismatch(wanted, given) =>
                PrintLine("refused   {} placeholders but {} values", wanted, given),
            else => PrintLine("refused   {}", error)
        }
    }
}

func Main() -> ! FormatError {
    var system = SystemAllocator();
    let allocator: Allocator = system;

    // Each call produces a String that lives on after it, unlike a printed line.
    let first = InvoiceCode(allocator, 7)?;
    let second = InvoiceCode(allocator, 42)?;
    PrintLine("codes     {} and {}, {} bytes each", first, second, first.Length());

    // A pattern that works, then three ways a pattern can be wrong, each refused with its own
    // case. `PrintLine` refuses the same patterns too, but its `IoError` can only say the
    // request was invalid, not where or why.
    Show(Render(allocator, "{} of {}", 3, 10));
    Show(Render(allocator, "[{:>>>}]", 3));
    // The compiler counts the placeholders of a pattern written in the call itself, so
    // `Render(allocator, "{}, {} and {}", 3, 4)` does not build: "format string has 3
    // placeholders, but 2 arguments were provided". A pattern that arrives as a value is
    // counted only when it runs.
    let pattern = "{}, {} and {}";
    Show(Render(allocator, pattern, 3, 4));
    Show(Render(allocator, "{:x}", "text"));
}

Besides Io, its Rux.toml lists Allocator, Format and Text under [Dependencies].

Run it

cd Examples/Text/Render
rux run
codes     INV-0007 and INV-0042, 8 bytes each
rendered  3 of 10
refused   bad spec at byte 5
refused   3 placeholders but 2 values
refused   unsupported formatting request

Common mistakes

Using the result without ?.
let first = InvoiceCode(allocator, 7); leaves first a String ! FormatError. Its next use fails: error: type 'String ! FormatError' has no field 'Length'.
A literal pattern with the wrong number of values.
Render(allocator, "{}, {} and {}", 3, 4) fails while compiling: error: format string has 3 placeholders, but 2 arguments were provided.
Matching FormatError without else.
Leave out the else arm and the inner match fails with error: match on 'FormatError' is not exhaustive; missing FormatError::UnsupportedRequest, FormatError::InsufficientCapacity, …. Handle the cases you care about and let else print the rest.

Try it yourself

  1. Render a price with two digits after the point, "{:.2}", and print its Length().
  2. Write Label(allocator: Allocator, name: char8[..], score: int) -> String ! FormatError that produces lines such as Ada.......17.
  3. Pass Render(allocator, "{:q}", 5) to Show. Which case is it?
  4. Change "[{:>>>}]" to "[{:>>}]". What does it render?

Learn more