Interfaces · Lesson 12.4

Display

Source
Make a type of your own printable with {} by implementing Display, whose WriteDisplay returns ! FormatError.

PrintLine("{}", value) has printed numbers, text and booleans since the very first lessons. That is not special treatment for built-in types: each of them implements one interface, Display from the Text package. A type of your own prints the same way as soon as it implements Display too. This lesson gives a Money type the text $12.05.

The Display interface

Display asks for a single method. In the Text package it is declared as:

func WriteDisplay(writer: &var TextWriter, spec: FormatSpec) -> ! FormatError;
PartWhat it is
writer: &var TextWriterWherever the text is going — the console, a string, a buffer. An interface parameter, borrowed writable because writing moves it along.
spec: FormatSpecWhat the placeholder asked for, such as the width in {:>8}.
-> ! FormatErrorWriting can fail — the console may be closed, a buffer may be full — so the method is a unit fallible.

These names come from two packages, so the lesson imports them, and its Rux.toml lists Format and Text:

import Format::WriteFormat;
import Io::PrintLine;
import Text::{ Display, FormatError, FormatSpec, TextWriter, WriteBytes };

Writing the text

extend Money : Display {
    // `spec` carries what the placeholder asked for, such as the width in `{:>8}`. This
    // implementation ignores it, which a type is free to do.
    func WriteDisplay(self: &Money, writer: &var TextWriter, spec: FormatSpec) -> ! FormatError {
        var cents = self.cents;
        if cents < 0 {
            // One write that may fail. `?` hands a failure to the caller and stops here.
            WriteBytes(writer, "-")?;
            cents = -cents;
        }
        // `WriteFormat` is PrintLine's twin: the same placeholders, written into the writer.
        // `{:02}` pads the cents to two digits with zeros, so 5 cents shows as `05`.
        return WriteFormat(writer, "${}.{:02}", cents / 100, cents % 100);
    }
}

The method is built from two writes, and either may fail:

  • WriteBytes(writer, "-")? writes the minus sign. The ? from Propagate passes a failure straight back to the caller and stops there.
  • return WriteFormat(…) writes the rest and hands back whatever that write reported — success or failure.

WriteFormat takes the same placeholders as PrintLine. Here {:02} pads the cents to two digits, so 5 cents becomes 05 and the coin prints as $0.05.

What happens when you print

flowchart LR
    p["PrintLine with a {}<br/>and a Money"] --> d["Money's WriteDisplay"]
    d --> w1["WriteBytes: '-'<br/>(only when negative)"]
    w1 --> w2["WriteFormat: '$12.05'"]
    w2 --> out["the console"]
    w1 -.->|"a failed write<br/>returns early"| p

PrintLine checks every argument against Display — its arguments are, in effect, a list of interface values. Money now passes, so it goes into a placeholder like any number would:

PrintLine("price:  {}", price);
PrintLine("refund: {}", refund);
PrintLine("coin:   {}", coin);
PrintLine("{} back from {}", refund, price);

Ignoring the spec

spec carries the placeholder's width, alignment, fill and precision. Money ignores it, which a type is free to do, but it has a visible effect: PrintLine("[{:>10}]", price) prints [$12.05] with no padding, because nothing in WriteDisplay applies the width. The Format lesson covers what those placeholder options mean.

The program

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

Src/Main.rux
// `PrintLine("{}", value)` prints numbers, text and booleans because each of those types
// implements one interface, `Display` from the Text package. A type of your own prints the same
// way as soon as it implements `Display` too.
//
// `Display` asks for a single method, `WriteDisplay`. It is handed a writer, which stands for
// wherever the text is going, and it writes the value's text into it. Writing can fail (the
// console may be closed, a buffer may be full), so the method returns `! FormatError`, and a
// failure from any step is passed straight on with `?` or `return`.
import Format::WriteFormat;
import Io::PrintLine;
import Text::{ Display, FormatError, FormatSpec, TextWriter, WriteBytes };

// An amount of money, kept as whole cents so nothing is lost to rounding.
struct Money {
    cents: int64;
}

extend Money : Display {
    // `spec` carries what the placeholder asked for, such as the width in `{:>8}`. This
    // implementation ignores it, which a type is free to do.
    func WriteDisplay(self: &Money, writer: &var TextWriter, spec: FormatSpec) -> ! FormatError {
        var cents = self.cents;
        if cents < 0 {
            // One write that may fail. `?` hands a failure to the caller and stops here.
            WriteBytes(writer, "-")?;
            cents = -cents;
        }
        // `WriteFormat` is PrintLine's twin: the same placeholders, written into the writer.
        // `{:02}` pads the cents to two digits with zeros, so 5 cents shows as `05`.
        return WriteFormat(writer, "${}.{:02}", cents / 100, cents % 100);
    }
}

func Main() -> int {
    let price = Money { cents: 1205 };
    let refund = Money { cents: -350 };
    let coin = Money { cents: 5 };

    // Now Money goes into a placeholder like any number would.
    PrintLine("price:  {}", price);
    PrintLine("refund: {}", refund);
    PrintLine("coin:   {}", coin);
    PrintLine("{} back from {}", refund, price);

    // Take away the `extend` and every line above is refused: "argument 2 to 'PrintLine' has
    // type 'Money', but variadic parameter 'args' requires 'Display'".
    return 0;
}

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

Run it

cd Examples/Interfaces/Display
rux run
price:  $12.05
refund: -$3.50
coin:   $0.05
-$3.50 back from $12.05

Common mistakes

Printing a type that does not implement Display.
Take away the extend and every PrintLine with a Money in it is refused: error: argument 2 to 'PrintLine' has type 'Money', but variadic parameter 'args' requires 'Display'.
Dropping a write's result.
WriteBytes(writer, "-"); without the ? fails with error: fallible result of type '! FormatError' is discarded. Every write can fail, so each one is either passed on with ? or returned.
Forgetting the return on the last write.
WriteFormat(writer, …); on its own line is a discarded fallible too, with the same error. Write return WriteFormat(…);, so its success or failure becomes the method's own.

Try it yourself

  1. Give struct Point { x: int32; y: int32; } a Display that prints (1, 2).
  2. Print price with PrintLine("[{:>10}]", price) and explain why no padding appears.
  3. Make a Temperature with a celsius: int32 field print as 21 C, and -4 C below zero.

Learn more

  • Format — widths, alignment and fill in placeholders
  • Render — formatting into a String you keep
  • Propagate — the ? used on each write
  • The Text and Format packages in the API reference