String
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
StringView | String | |
|---|---|---|
| Holds | a view of someone else's bytes | its own copy of the bytes |
| Making one | free: no copy, no allocation | copies the bytes into allocated memory |
| Can fail? | only FromBytes, with none | yes: String ! TextError |
| Lives | no longer than the bytes it views | as long as the String itself |
| Cleans up | nothing to clean up | its 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 case | When |
|---|---|
OutOfMemory | the allocator could not supply the storage |
InvalidUtf8 | FromBytes was given bytes that are not text |
LengthOverflow | a length passed what a uint can hold |
NotABoundary | a 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.
// 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
?.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'.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.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
- Reverse
"né"and handle the failure with amatchon.Successand.Failureinstead of?, printing a message of your own. - Make a
Stringfrom a view withString::FromView(allocator, view). - Use
Substring(start, end)to copy the first four bytes ofsavedinto a newString. - Compare
wordandsavedwithEquals, before and afterReplaceWith.
Learn more
- Text in the API reference
- String view — the borrowed form a
Stringlends out - Destructor and Custom copy — how a
Stringfrees and copies its memory - Allocator — where the memory comes from