Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

String interpolation

An interpolated string is a string literal with a $ before its opening quote. Inside it, an expression in braces, {name} or {a + b}, is replaced by the text of its value:

# Interpolated strings: `$"..."` with expressions in braces.
main(): int {
    var name = "Pepper";
    var count = 3;
    var ready = true;

    # Each {name} is replaced by that variable's text.
    var line = $"{name} has {count} items, ready: {ready}";
    assert(line == "Pepper has 3 items, ready: true", "integers, bool and String");

    # Any expression fits between the braces, strings included.
    var sizes = [2, 5];
    assert($"total {sizes[0] + sizes[1]}" == "total 7", "arithmetic and index");
    assert($"{name.size} letters" == "6 letters", "call");
    assert($"{count > 1 ? "many" : "one"}" == "many", "ternary with strings");

    # Doubled braces are literal braces; escapes work as in any string.
    assert($"{{count}} = {count}" == "{count} = 3", "literal braces");
    assert($"tab\t{count}\n" == "tab\t3\n", "escapes");
    return 0;
}
  • {expression} holds an expression: a name (a local, a parameter, a global, or inside a method a field), a member access ({p.x}), a call ({f(x)}), an index ({items[0]}), arithmetic ({a + b}), or a ternary ({a > b ? a : b}). Spaces around it are ignored.
  • The expression changes no variable: an assignment (=, +=, …), ++ or -- anywhere in it is an error, even inside a call’s arguments ({f(a = 3)}). Only a closure’s body, which does not run in place, may hold one. A function it calls may still have effects of its own.
  • String literals may appear inside the braces ({greet("you")}), and so may character literals and other interpolated strings. A } inside them, or inside brackets of the expression’s own, does not end it.
  • The expression fits on one line and holds no comment: a line break or a # inside the braces is an error.
  • A format specification may follow the expression after a :, as in {n:>8} or {n:08x}: see below.
  • {{ and }} are a literal { and }.
  • Escapes (\n, \t, \", …) work as in any string literal.
  • The result is a new String. An interpolated string with no braces is an ordinary string.

Which values can be interpolated

Each value is converted with to_string(value), so it can be:

  • an integer of any size, signed or unsigned, which gives its decimal text;
  • a float or double, which gives the shortest decimal text that reads back as the same number, as Swift prints one: 0.1, 3.0 (a whole number keeps its .0), -2.5, and an exponent outside 1e-4 to 1e16 (1e+16, 1.5e-05); nan, inf and -inf for the special values;
  • a bool, which gives true or false;
  • a tuple of up to eight values that can each be interpolated, which gives its elements’ text in parentheses, (1, two) (a String element without quotes);
  • a String, which is used as it is;
  • a value whose type has a to_string(): String method, such as a struct that implements the runtime’s ToString trait, or a value of type ToString itself;
  • a value whose type has a to_string<T> override. Like hash<Point> (see overriding a generic function), an override applies to the whole program, so it serves types that have no to_string() method of their own.
# A struct is interpolated through its to_string() method.
struct Point {
    var x: int = 0;
    var y: int = 0;
    construct(this.x = 0, this.y = 0) {}
}

extend Point with ToString {
    to_string(): String {
        # Inside a method, fields are names too.
        return $"({x}, {y})";
    }
}

# A type without the method gets a to_string<T> override instead.
struct Celsius {
    var degrees: int = 0;
    construct(this.degrees = 0) {}
}

to_string<Celsius>(value: Celsius): String {
    return $"{value.degrees} C";
}

main(): int {
    var start = Point(1, 2);
    var reading = Celsius(21);
    assert($"from {start}" == "from (1, 2)", "ToString");
    assert($"it is {reading}" == "it is 21 C", "override");

    # A trait value converts through the value's own method.
    var shown: ToString = start;
    assert($"{shown}" == "(1, 2)", "trait value");
    return 0;
}

Numbers and tuples, with the format specifications below:

# Floating-point numbers and tuples have text too.
main(): int {
    var price = 4.5;
    var ratio = 1.0 / 3.0;
    assert($"{price}" == "4.5", "the shortest text that reads back");
    assert($"{2.0}" == "2.0", "a whole number keeps its point");
    assert($"{1.0e20}" == "1e+20", "large numbers use an exponent");
    assert($"{ratio:.3}" == "0.333", "a precision rounds to that many digits");
    assert($"[{price:8.2}]" == "[    4.50]", "numbers align right");
    assert($"{price:+08.2}" == "+0004.50", "with a sign and zeros");

    var point = (3, "north");
    assert($"{point}" == "(3, north)", "a tuple's elements in parentheses");
    print("{0} at {1:.1}", point, price);   # (3, north) at 4.5
    return 0;
}

Runtime types such as String, StringBuilder, io.Path, Uri and regex.Match implement ToString. An enum is interpolated through a to_string() method it declares (return name(); gives the case’s name).

A plain overload such as to_string(p: Point) is not used: the runtime’s code does not import your file, so it cannot see yours (see overloading). Write a to_string<Point> override or a method instead.

Format specifications

A : after the expression starts a format specification, which pads the value’s text, writes an integer in another base, or rounds a floating-point number to a number of digits. Its parts, all optional, come in this order:

{value:[[fill]align][+][0][width][.precision][type]}
PartMeaning
align< left, ^ centered (leaning left), > right. Without one, numbers go to the right and everything else to the left.
fillThe character the padding is made of, written before align; a space by default. It can be any character but {, }, ", ' and #.
+Writes a + before a number that is not negative.
0Pads with zeros after the sign (-0042). It cannot be combined with align.
widthThe least number of characters the text takes; longer text is kept whole.
.precisionFor a float or double: the number of digits after the point, rounded to nearest, a tie to the even digit ({pi:.2} is 3.14, {0.125:.2} is 0.12; .0 leaves no point). At most 100.
typed decimal, x and X hexadecimal (lower and upper case), b binary, o octal.
# Format specifications: padding, signs and other bases after a `:`.
main(): int {
    var count = 42;
    var name = "Pepper";

    # Width and alignment pad any value; numbers go right by default.
    assert($"[{count:6}]" == "[    42]", "a number goes right");
    assert($"[{name:10}]" == "[Pepper    ]", "text goes left");
    assert($"[{name:>10}]" == "[    Pepper]", "right");
    assert($"[{name:*^10}]" == "[**Pepper**]", "centered with a fill");

    # Integers take a sign, zero padding and a base.
    assert($"{count:+}" == "+42", "a plus sign");
    assert($"{count:05}" == "00042", "zero padding");
    assert($"{255:x} {255:X} {5:b} {8:o}" == "ff FF 101 10", "bases");
    assert($"0x{255:08x}" == "0x000000ff", "padded hexadecimal");
    assert($"{-42:x}" == "-2a", "a negative number keeps its sign");

    # The `:` after the whole expression starts the specification.
    assert($"[{count > 1 ? "many" : "one":>5}]" == "[ many]", "a ternary");
    return 0;
}
  • Padding (fill, align and width) works for any value that can be interpolated: it pads the value’s to_string text, counting characters, not bytes.
  • + and 0 need a number: an integer (of any size, signed or unsigned), or a float or double given a precision ({x:+08.2}). A type needs an integer, and a precision a float or double. Using one with any other value is an error at that character, and a precision cannot be combined with a type.
  • A negative number keeps its sign in every base: {-42:x} is -2a, not the bits of its two’s complement.
  • The : that starts the specification is the one after the whole expression, so a ternary can be formatted as it is: {n > 1 ? "many" : "one":>5}.

Formatting by position: String.format

String.format takes a string literal whose braces name its other arguments by position, {0} for the first, each with an optional format specification, and returns the text:

# String.format: braces name the arguments by position.
main(): int {
    var a = 2;
    var b = 3;
    assert(String.format("{0} + {1} = {2}", a, b, a + b) == "2 + 3 = 5", "in order");

    # An index can repeat or come out of order, with its own specification.
    assert(String.format("{1} before {0}", "first", "second") == "second before first", "reordered");
    assert(String.format("{0} is {0:x} in hex", 255) == "255 is ff in hex", "repeated");
    assert(String.format("[{0:>4}] {{literal}}", a) == "[   2] {literal}", "padding and braces");
    return 0;
}
  • The format must be a string literal: the compiler reads it, so an index out of range, an argument no brace names, or a bad specification is an error at compile time. {{ and }} are literal braces, as in an interpolated string.
  • Each argument is evaluated once, from left to right, however many braces name it and in whatever order.
  • An index can repeat, to show one value twice or in two forms ("{0} is {0:x} in hex").
  • It is part of the compiler, not a function in the runtime: it cannot be stored in a variable or called through a function value.
  • print takes the same arguments: print("{0} of {1}", done, total) prints what String.format makes of them.

Errors

A value with no conversion is reported at its braces, with a note at its type:

main.pep:5:17: error: cannot interpolate a value of type 'Foo': it has no 'to_string()' method and no 'to_string' overload
    5 |     print($"f = {f}");
      |                 ^~~
main.pep:1:8: note: declare 'to_string(): String' in 'Foo', or implement 'ToString' with 'extend Foo with ToString'
    1 | struct Foo {}
      |        ^~~

A lone brace is an error, since doubling it is how to write one, and so is anything after the expression but its }:

main.pep:2:15: error: unmatched '}' in interpolated string; write '}}' for a literal '}'
    2 |     print($"a } b");
      |               ^
main.pep:3:18: error: expected '}'
    3 |     print($"a {x b");
      |                  ^
main.pep:3:15: note: to match this '{'
    3 |     print($"a {x b");
      |               ^

Empty braces are reported where the expression should start:

main.pep:2:16: error: expected an expression after '{' in interpolated string; write '{{' for a literal '{'
    2 |     print($"a {} b");
      |                ^

An error in the expression itself, such as a name that is not declared (use of undeclared identifier 'x'), is reported like any other, at the token inside the braces.

A format specification’s errors are reported at the character in it that is wrong:

main.pep:5:16: error: unexpected 'q' in format specification; expected '[[fill]align][+][0][width][.precision][type]'
    5 |     print($"{n:q}");
      |                ^
main.pep:5:19: error: cannot format a value of type 'String' as an integer
    5 |     print($"{name:x}");
      |                   ^
main.pep:6:16: error: cannot format a value of type 'i32' with a precision; only 'float' and 'double' take one
    6 |     print($"{n:.2}");
      |                ^

String.format reports an index with no argument at the index, and an argument with no index at the argument:

main.pep:5:31: error: argument index 2 is out of range for 2 arguments
    5 |     print(String.format("{0} {2}", n, n));
      |                               ^
main.pep:5:35: error: argument 1 of 'String.format' is not used by its format string
    5 |     print(String.format("{0}", n, n + 1));
      |                                   ^~~~~

Changing a variable inside the braces is reported at the operator:

main.pep:3:16: error: an assignment cannot appear in an interpolated string
    3 |     print($"{a += 1}");
      |                ^~

How it works

The compiler turns $"({x}, {y + 1})" into "(" + to_string(x) + ", " + to_string(y + 1) + ")", resolving to_string in the runtime rather than in your file. So a struct’s own to_string() method can interpolate its fields without calling itself, and the result costs what the same + expression does. A value with a format specification goes through a runtime call that pads or converts its text instead. String.format becomes the same concatenation, with its arguments bound to hidden names first when the braces name them out of order or more than once.