Text · Lesson 14.4

String

Source
Own text with String, which keeps its bytes in allocated memory, and copy it with Clone, which returns String ! TextError.

A view borrows, so it can never outlive the bytes it looks at. Text that a function builds and hands back, or that must be kept after its source is gone, needs an owner. That is String: it keeps its own copy of the bytes, in memory it asked an allocator for, and gives that memory back in its destructor when the value's life ends.

Borrowed or owned

StringViewString
Holdsa view of someone else's bytesits own copy of the bytes
Making onefree: no copy, no allocationcopies the bytes into allocated memory
Can fail?only FromBytes, with noneyes: String ! TextError
Livesno longer than the bytes it viewsas long as the String itself
Cleans upnothing to clean upits destructor frees the memory

Where the memory comes from

A String needs an allocator to ask for memory. Main makes the system's allocator and passes it on as an interface value of type Allocator:

var system = SystemAllocator();
let allocator: Allocator = system;

The allocator itself is the subject of the Memory part. For now it is simply where a String's bytes come from.

Returning text a function made

Reversed builds its letters in a local array, which is gone once the function returns. A view of that array would be left pointing at nothing, so the function returns a String that owns a copy:

func Reversed(allocator: Allocator, word: char8[..]) -> String ! TextError {
    var letters: char8[16];
    for i in 0..word.length {
        letters[i] = word[word.length - 1 - i];
    }
    return String::FromBytes(allocator, letters[..word.length]);
}

Two things can go wrong, so the result is a fallible String ! TextError:

TextError caseWhen
OutOfMemorythe allocator could not supply the storage
InvalidUtf8FromBytes was given bytes that are not text
LengthOverflowa length passed what a uint can hold
NotABoundarya position fell inside a character rather than between two

The UTF-8 check is not a formality here. Reversed byte by byte, né puts the two bytes of é in the wrong order, and FromBytes fails with TextError::InvalidUtf8 rather than make a String that is not text.

Main is a fallible main, func Main() -> ! TextError, so ? passes any such failure on and the program exits with status 1:

var word = Reversed(allocator, "stressed")?;

Copying with Clone

Clone makes an independent copy with storage of its own, and it can fail too, because the copy needs memory:

let saved = word.Clone()?;
word.ReplaceWith(Reversed(allocator, "drawer")?);

ReplaceWith gives word new text and frees its old bytes. saved has its own copy, so it is untouched: word is now reward, saved is still desserts.

Plain assignment, let copy = saved;, would copy too — a String has a custom copy. But assignment has no way to report failure, so it ends the program if memory runs out. Clone hands that failure back as a TextError, which is why it is the one to use when failure must be handled.

Lending a view

Whenever borrowed text is enough, a String lends out a view without copying anything:

let view = saved.View();
PrintLine("view   {}, {} characters", view, view.ScalarCount());

The view is valid as long as saved is. No cleanup is written anywhere: word and saved each release their own memory at the end of Main, as the destructor lesson promised.

The program

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

Src/Main.rux
// A view borrows, so it can never outlive the bytes it looks at. Text that a function builds
// and hands back, or that must be kept after its source is gone, needs an owner. That is
// `String`: it keeps its own copy of the bytes, in memory it asked an allocator for, and gives
// that memory back in its destructor when the value's life ends.
//
// Asking for memory can fail, and so can checking that bytes are UTF-8, so the calls that
// make a `String` return `String ! TextError`. `Main` is fallible here, and `?` passes any such
// failure on. The allocator itself is the subject of the Memory part; for now it is simply
// where a String's bytes come from.
import Allocator::{ Allocator, SystemAllocator };
import Io::PrintLine;
import Text::{ String, TextError };

// The reversed letters exist only in this function's array, which is gone once it returns. A
// view of them would be left pointing at nothing, so the function returns a String that owns
// a copy. `FromBytes` checks the bytes are text before copying them, and that check matters
// here: reversed byte by byte, "né" puts the two bytes of `é` in the wrong order, and the
// call fails with `TextError::InvalidUtf8`. The array has room for words of up to 16 bytes.
func Reversed(allocator: Allocator, word: char8[..]) -> String ! TextError {
    var letters: char8[16];
    for i in 0..word.length {
        letters[i] = word[word.length - 1 - i];
    }
    return String::FromBytes(allocator, letters[..word.length]);
}

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

    var word = Reversed(allocator, "stressed")?;
    PrintLine("word   {}, {} bytes", word, word.Length());

    // `Clone` makes an independent copy with storage of its own. Replacing the original's text
    // afterwards leaves the clone exactly as it was.
    let saved = word.Clone()?;
    word.ReplaceWith(Reversed(allocator, "drawer")?);
    PrintLine("word   {}", word);
    PrintLine("saved  {}", saved);

    // `let copy = saved;` would also copy, but plain assignment has no way to report failure,
    // so it ends the program if memory runs out. `Clone` hands that failure back as a
    // `TextError` instead, which is why it is the one to use when failure must be handled.

    // A String lends out a view whenever borrowed text is enough, without copying anything.
    let view = saved.View();
    PrintLine("view   {}, {} characters", view, view.ScalarCount());

    // No cleanup is written: `word` and `saved` each release their own memory at the end of
    // `Main`, as the Destructor lesson promised.
}

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

Run it

cd Examples/Text/String
rux run
word   desserts, 8 bytes
word   reward
saved  desserts
view   desserts, 8 characters

Common mistakes

Forgetting the ?.
var word = Reversed(allocator, "stressed"); compiles on its own, but word is then a String ! TextError, not a String. The next use fails: error: type 'String ! TextError' has no field 'Length', and printing it fails with has type 'String ! TextError', but variadic parameter 'args' requires 'Display'.
Returning a slice of a local array.
A function declared -> char8[..] that ends with return letters[..word.length]; compiles — but the slice points into an array that no longer exists once the function returns, and whatever it reads afterwards is anyone's guess. Text a function makes must be returned as a String, which owns it.
Ignoring that FromBytes checks.
Change "drawer" to "né" and the program prints its first line, then stops with exit status 1: the reversed bytes are not UTF-8, FromBytes fails, and ? passes InvalidUtf8 out of Main.

Try it yourself

  1. Reverse "né" and handle the failure with a match on .Success and .Failure instead of ?, printing a message of your own.
  2. Make a String from a view with String::FromView(allocator, view).
  3. Use Substring(start, end) to copy the first four bytes of saved into a new String.
  4. Compare word and saved with Equals, before and after ReplaceWith.

Learn more