Render
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:
| Case | When |
|---|---|
InvalidSpecification(at) | a spec does not parse; at is the byte where it stopped |
UnsupportedRequest | a spec parses, but asks for something this value does not support |
ArgumentCountMismatch(wanted, given) | placeholders and values do not match in number |
InsufficientCapacity | a destination of fixed size cannot take the whole text |
ValueOutOfRange | a valid value cannot be shown in the form asked for |
TextFailure(kind) | text handling failed — this is where running out of memory arrives |
WriterFailure | the 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"));
| Pattern | Values | Result |
|---|---|---|
"{} of {}" | 3, 10 | 3 of 10 |
"[{:>>>}]" | 3 | InvalidSpecification(5) — >> is a fill and an alignment; the third > fits nowhere |
pattern | 3, 4 | ArgumentCountMismatch(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.
// `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
?.let first = InvoiceCode(allocator, 7); leaves first a String ! FormatError. Its next use fails: error: type 'String ! FormatError' has no field 'Length'.Render(allocator, "{}, {} and {}", 3, 4) fails while compiling: error: format string has 3 placeholders, but 2 arguments were provided.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
- Render a price with two digits after the point,
"{:.2}", and print itsLength(). - Write
Label(allocator: Allocator, name: char8[..], score: int) -> String ! FormatErrorthat produces lines such asAda.......17. - Pass
Render(allocator, "{:q}", 5)toShow. Which case is it? - Change
"[{:>>>}]"to"[{:>>}]". What does it render?
Learn more
- Format in the API reference
- Format and Format number — the pattern language
- String — what
Renderreturns - Variant match — taking
FormatErrorapart
14.10 Format number
Spell numbers with a placeholder spec: digits after the point, rounding, scientific notation, number bases, zero padding and an explicit sign.
14.12 Parse
Read a number from text with ParseInt32, which returns int32 ! ParseError and says where malformed or overflowing text went wrong.