rokdocs
v0.1

2.10. fmt - Formatting traits

The rok::core::fmt module defines the family of traits that convert values to human-readable strings. Every format spec in an f-string - f"{x}", f"{x:?}", f"{n:08b}", and so on - dispatches to the traits in this chapter.

The lexical decomposition in refs-lang 1.7.4 f-string format spec and the trait dispatch in this chapter together determine the meaning of an f-string.

2.10.1. Dispatch

An f-string hole {expr[:spec]} calls Debug::fmt if the spec's type is ?, and Display::fmt otherwise. The rest of the spec (fill / align / sign / # / 0 / width / precision and b o x X e E f) is carried by the Formatter.

  • Numbers (all integers, floating-point types, int, float) apply the spec directly to their own output (2.10.5, 2.10.6).
  • The fmt of any other value does not look at the spec (f.width() is none). The value writes itself as is; the Formatter then applies fill / align / width and truncates by precision.

d / g / s are other names for the default notation. The meaning of each symbol and flag is defined in refs-lang 1.7.3. An f-string does not build an intermediate string per hole; it writes each hole in turn into the buffer of a single Formatter.

2.10.2. Formatter

struct Formatter { .. }                       // output buffer + current spec
enum Alignment { Left, Right, Center, Equal }
enum Sign { Plus, Minus, Space }

impl Formatter {
    fn new() -> Formatter;
    fn with_capacity(n: usize) -> Formatter;
    fn with_spec(spec: FormatSpec) -> Formatter;
    fn finish(self) -> str;                   // text written so far

    fn write_str(ref self, s: str) throws (FmtError);
    fn write_char(ref self, c: char) throws (FmtError);
    fn pad(ref self, s: str) throws (FmtError);             // applies fill / align / width / precision
    fn pad_integral(ref self, is_nonnegative: bool, prefix: str, digits: str) throws (FmtError);
                                                            // sign + # prefix + 0 / fill padding

    fn fill(self) -> char;
    fn align(self) -> Alignment | none;       // none: default (right for numbers, left otherwise)
    fn sign(self) -> Sign | none;
    fn alternate(self) -> bool;               // #
    fn pad_zero(self) -> bool;                // 0
    fn width(self) -> usize | none;
    fn precision(self) -> usize | none;
}

FormatSpec is a parsed spec value. A static spec is parsed at compile time; a dynamic spec (one containing {..} / ${..}) is parsed once at runtime.

struct FormatSpec {
    fill: char; align: Alignment | none; sign: Sign | none; alt: bool; zero: bool;
    width: usize | none; precision: usize | none; ty: char;
}
impl FormatSpec {
    fn new() -> FormatSpec;                   // empty spec
    fn parse(text: str) -> FormatSpec;        // panics on an invalid spec
}

2.10.3. Display

#[lang("display_trait")]
trait Display {
    fn fmt(self, ref f: Formatter) throws (FmtError);
}

This is the default dispatch for f"{x}". The fmt of a primitive number applies the spec and writes in a single runtime call.

impl Display for str {
    fn fmt(self, ref f: Formatter) throws (FmtError) { try f.write_str(self) }
}

struct Point { x: f64; y: f64; }

impl Display for Point {
    fn fmt(self, ref f: Formatter) throws (FmtError) {
        try f.write_str("(");
        try Display::fmt(self.x, ref f);
        try f.write_str(", ");
        try Display::fmt(self.y, ref f);
        try f.write_str(")");
    }
}

f"{Point { x: 1.5, y: 2.5 }}";       //→ "(1.5, 2.5)"
f"{Point { x: 1.5, y: 2.5 }:>12}";   //→ "  (1.5, 2.5)"   (f.width() inside Point::fmt is none)
  • The call site requires ref (Display::fmt(x, ref f)). The form x.fmt(f) is ambiguous for a type that implements both Display and Debug, so the call is written as a path.
  • Display is not derived. The type's designer decides its representation.

2.10.4. Debug

#[lang("debug_trait")]
trait Debug {
    fn fmt(self, ref f: Formatter) throws (FmtError);
}

impl Debug for str { .. }    // a valid rok string literal
impl Debug for char { .. }   // a valid rok character literal

This is the dispatch for f"{x:?}"; it aims for a representation that can be read back. The Debug output of str / char is a rok literal. Quotes, \\, \n / \r / \t, and so on are escaped; other control characters and bidi characters (U+202A..U+202E, U+2066..U+2069, etc.) are written as \u{..}.

Display vs Debug

The two traits serve different purposes, summarized in the following table.

AspectDisplayDebug
Audienceend userdeveloper
Accuracyreadability firstno information loss (aims to round-trip)
Quotesstring as is (no quotes)string wrapped in "..." (with escapes)
Containersusually not definedautomatic for every collection
derivenot allowed (manual impl)allowed (#[derive(Debug)])
let s = "hello\nworld";
let d1 = f"{s}";        //→ hello
                        //   world          (Display - as is)
let d2 = f"{s:?}";      //→ "hello\nworld"  (Debug - escaped)

User-defined / derive

#[derive(Debug)]
struct Point { x: f64; y: f64; }

let p = Point { x: 1.5, y: 2.5 };
let s = f"{p:?}";              //→ "Point { x: 1.5, y: 2.5 }"

Debug can be derived automatically by composing the Debug output of every field.

2.10.5. Integer spec

typeInteger
(none), d, g, sdecimal
ffixed point ({5:.2f} → 5.00)
b o x Xbinary / octal / hex (0b / 0o / 0x with #)
e Eexponent notation

Giving b o x X to a floating-point value is a compile error (static spec) or a panic (dynamic spec). A negative fixed-width integer prints as the two's complement at that type's width ({-1i8:x} is "ff"). int is written as sign and magnitude (the int value -255 with {n:x} is "-ff").

let n: u32 = 0xCAFE;
f"{n:b}";        //→ "1100101011111110"
f"{n:#x}";       //→ "0xcafe"
f"{n:X}";        //→ "CAFE"
f"{42:08b}";     //→ "00101010"
f"{42:e}";       //→ "4.2e1"

2.10.6. Floating-point output

  • {} writes the shortest digit string that reads back to the same value, and does not use exponent notation. 1.0 is 1, 0.1 is 0.1, NaN is NaN, infinity is inf / -inf.
  • {:e} writes the same digits in exponent notation (0.15 is 1.5e-1). {:E} uses E.
  • With a precision, the value is rounded to that many digits ({x:.3e}, {x:.2}).
  • A promoted float (exponent outside f64 range) is written in exponent notation even with {} (1.5e400).
let x: f64 = 12345.678;
f"{x}";          //→ "12345.678"
f"{x:e}";        //→ "1.2345678e4"
f"{x:.3e}";      //→ "1.235e4"
f"{1.0}";        //→ "1"

2.10.7. Full f-string format spec grammar

The authority is refs-lang 1.7.3; this is a summary.

spec      = [fill][align][sign][#][0][width][.precision][type]
align     = "<" | ">" | "^" | "="
sign      = "+" | "-" | " "
type      = "" | "s" | "d" | "g" | "f" | "?" | "b" | "o" | "x" | "X" | "e" | "E"
width     = unsigned-int | hole
precision = unsigned-int | hole
hole      = "{" expr "}" | "${" expr "}"      // nested hole, no spec of its own
fill      = any single char (default: space)

Dynamic spec

let s1 = f"{x:.{prec}}";               //→ "3.1416"        (dynamic precision)
let s2 = f"{x:>${width}.${prec}}";     //→ "    3.1416"    (dynamic width + precision)
let s3 = f"{x:{full_spec}}";           //→ ...             (entire spec dynamic)

$$ inside a spec is a single $. A $ in the body text is literal (f"cost: $100"). A static spec is checked at compile time; if a dynamic spec expands to an invalid result, it panics at runtime. The lexer's tokenization follows "Lexical processing" in refs-lang 1.7.3.

2.10.8. align / fill / width / precision

Formatter::pad handles the spec's align, fill and width automatically, so a user impl only produces the content.

Without an explicit align, numbers (integers, floating-point) are right-aligned and all other types are left-aligned. When < / > / ^ (and fill) are given, any type follows that alignment. ? (Debug) also accepts width / align.

let n: i32 = 42;

f"{n}";              //→ "42"            (default)
f"{n:5}";            //→ "   42"         (width 5, numbers right-aligned by default)
f"{n:<5}";           //→ "42   "         (left)
f"{n:>5}";           //→ "   42"         (right)
f"{n:^5}";           //→ " 42  "         (center)
f"{n:0>5}";          //→ "00042"         (fill 0)
f"{n:*^7}";          //→ "**42***"       (fill *, center, width 7)
f"{n:=+5}";          //→ "+  42"         (= : pad between sign and digits, + : sign on positives)
f"{n:+=5}";          //→ "+++42"         (fill '+', align '=')
f"{-42:=6}";         //→ "-   42"
f"{-42:06}";         //→ "-00042"        (0 : pad with 0, = alignment)
f"{n:+}";            //→ "+42"           (+ : sign on positives too)
f"{"ab":5}";         //→ "ab   "         (strings left-aligned by default)

precision is applied the same way.

let x: f64 = 3.141592653589793;
f"{x:.2}";           //→ "3.14"          (2 digits after the point)
f"{x:.0}";           //→ "3"
f"{x:10.4}";         //→ "    3.1416"    (width 10, precision 4, numbers right-aligned by default)
f"{x:<10.4}";        //→ "3.1416    "    (left alignment requires explicit <)

Precision on a string is interpreted as truncation to a maximum length; precision on an integer is ignored.

let s = "hello world";
f"{s:.5}";           //→ "hello"
f"{n:.3}";           //→ "42"

2.10.9. Module mapping

The following table lists which traits each type implements.

TypeDisplayDebug
integers, int, floating-point, float✓✓
bool, char, str✓✓
List<T>, Set<T>✗✓ (T: Debug)
Dict<K, V>✗✓ (K, V: Debug)
(A, B, ..)✗✓ (all Debug)
T | none✗✓ (T: Debug)
Result<T, E>✗✓ (T, E: Debug)

Display exists only for scalars, strings and user types that implement it by hand; containers do not have it. Debug exists for almost every type and is produced by derive.

2.10.10. #[derive(Debug)]

#[derive(Debug)]
struct User { id: u64; name: str; }

let u = User { id: 1, name: "Alice" };
let s = f"{u:?}";          //→ User { id: 1, name: "Alice" }

The derive works as follows. A struct prints as TypeName { field1: <field1:?>, field2: <field2:?>, ... }, and a tuple struct prints as TypeName(<f1:?>, <f2:?>, ...). An enum prints as VariantName or VariantName(...) / VariantName { ... }. The derive requires every field to implement Debug (recursively).

There is no #[derive(Display)]. Deciding the representation is the user's responsibility.

2.10.11. REPL / print / automatic output

The REPL section of refs-lang 9.7 JIT cell execution flow defines the following.

> let x = 42;
> x
i32: 42

> 1 + 2
i32: 3

> "hello"
str: "hello"

When the REPL automatically prints the last expression, it prints type: value with the value formatted by Debug; for a type without Debug, only the type (refs-lang 9.7.7, 2.12.4).

By contrast, println and print dispatch only to Display.

println(f"x = {x}");          // Display
println(f"x = {x:?}");        // Debug (explicit spec)

2.10.12. FmtError

struct FmtError;

The error for a failed write to a stream. It does not occur when writing to memory (f-strings, Formatter::new, etc.). Every fmt method is throws (FmtError), so calls are marked with try to propagate.

2.10.13. Comparison with other languages

The following table shows where rok's fmt design sits relative to Rust and Python.

AspectRust Display/DebugPython __str__/__repr__rok
Human / debug split✓ separate traits✓ separate methods✓ separate traits
Debug derive#[derive(Debug)]automatic default#[derive(Debug)]
Display derivenot allowednot allowed (manual)not allowed
Radix / exponent6 traitsformat specFormatter carrying the spec
Dynamic specpositional args ({0:.*})f-string {:.{prec}}${...} expression ($-sigil)
Spec syntaxPython-likePEP 3101Python-like + ${...}

rok's most distinctive choice is to separate dynamic spec insertion visually with ${...}, avoiding the nested-brace ambiguity of Python's {:.{prec}}. The notation is also isomorphic to the $ sigil of JS, Kotlin and Scala.

2.10.14. Derive policy summary

Among the fmt traits, only Debug can be derived. Display is not a derive target: the user-facing surface is decided differently in each domain, so automatic derivation does not fit, and there is no meaningful default form for Display. Types that need a display representation should implement Display by hand.

Edit this page on GitHub