String
On this page
Names, messages, whatever someone typed: nearly every program works with text. Text that a Cone program builds and changes is a String. This page shows how to build one, ask questions of it and walk its characters, and it brings in the first value in the Guide that moves rather than copies. Here is a program that checks for a pangram, a sentence that uses every letter of the alphabet:
mod pangram;
import stdio use *;
import collections use *; // collections gives us String
// A line about 'text': how many of the alphabet's 26 letters it uses.
// &str is a view of a piece of text, wherever that text is kept
fn verdict(text &str) String {
// A String can change: this one starts as a copy of 'text',
// then has every capital letter made small
var lower = String.from(text);
lower.lowerAscii();
// Count each letter of the alphabet that turns up somewhere in it
var used = 0;
'next: each letter in "abcdefghijklmnopqrstuvwxyz".chars() {
each c in lower.chars() {
if c == letter { used += 1; continue 'next; }
}
}
// <- adds to the end of a String, just as it sends to print
var line = String.from(text);
line <- ": ", used, " of 26 letters";
if used == 26 { line <- ", a pangram"; }
line; // the finished String goes back to the caller
}
fn main() {
print <- verdict("The quick brown fox jumps over the lazy dog").view(), "\n";
print <- verdict("Pack my box with five dozen liquor jugs").view(), "\n";
print <- verdict("Hello, world").view(), "\n";
}
It prints:
The quick brown fox jumps over the lazy dog: 26 of 26 letters, a pangram Pack my box with five dozen liquor jugs: 26 of 26 letters, a pangram Hello, world: 7 of 26 letters
Try this: swap the lazy dog for a lazy cat, and see what it costs the fox.
String and &str#
A String comes from the collections package, which is why the program imports it. It owns the memory its text is kept in, and grows that memory as text is added. Text written into the program, a string literal, is different: it never changes, and it lasts as long as the program runs. Its type is &str, a view of a piece of text; the & marks a reference, and there is more on references below.
A function that only reads text takes a &str, as verdict does, and then any text will do: a literal as it is, or a String's text through its view(). print reads text the same way, which is why the program prints each String's view():
fn shout(text &str) {
print <- text, "!\n";
}
let motto = "less is more";
var title = String.from(motto); // a String holding a copy of the text
title.upperAscii();
shout(motto); // less is more!
shout(title.view()); // LESS IS MORE!
A String has every method of str as its own, so whatever you can ask of a literal you can ask of a String, and it has more besides: methods that change it, like upperAscii. Where a String is wanted and a literal is given, the literal's text is copied into a new String for you: let word String = "copied";.
Copy or move#
Assigning a number, or handing one to a function, copies it. Changing the copy leaves the original as it was:
let a = 3; var b = a; // b holds a copy of a's value b += 1; // b is 4, and a is still 3
A String is different. It owns the memory its text is kept in, and two copies would each believe that memory was theirs to change and to free. So a String moves instead: assigning it, or handing it to a function, passes its text on, and the variable it came from has nothing left. The compiler refuses any later use of it:
let plan = String.from("day two: the coast road");
let today = plan; // the text moves from plan to today
print <- today.view(), "\n"; // day two: the coast road
// print <- plan.view(); // Error: This variable's value has been moved out.
Nothing is copied when a String moves. Its text stays where it is in memory, and only the ownership passes on: that is how verdict hands its finished line back to main. And since a String only ever has one owner, Cone knows exactly when its text is no longer needed, and frees it then, with no garbage collector and nothing for you to free by hand.
Whether a value copies or moves is decided by its type, not by how it is used. Numbers copy, and a String moves; you will meet moving again with structs and collections. A type you write moves when it has a finalizer, when any of its fields moves, or when it says so by naming Move; every other type copies. Moves, in Recipes, has the rest.
Handing text on: a unique reference#
Once a String is finished, freeze() turns it into fixed text held by a unique reference, a Uni[str]: the one and only way to reach that text. The bytes are not copied; the reference now owns them, and they can no longer change. A unique reference can be passed around like any other value, from variable to variable and function to function, and because it is the only one, it moves as a String does:
fn announce(news Uni[str]) {
print <- "News: ", news, "\n";
}
var headline = String.from("Quick brown fox");
headline <- " jumps over lazy dog";
let news = headline.freeze(); // finished: the text now belongs to news
announce(news); // News: Quick brown fox jumps over lazy dog
// announce(news); // Error: news has already moved into announce
References, unique and otherwise, come back in depth later, in the Memory part.
Building a String with the send operator#
<- is the send operator: print <- x sends x to the output, and s <- x sends it to the end of s. Text is added as it is and a number as its digits, and one <- can send several, separated by commas, with no format to get wrong. Text built before it is printed can also be laid out first: padStart fills a String out to a width from the front, which lines numbers up on the right, and padEnd fills it from the back.
var table = String.empty(); // a String with nothing in it yet
each n in 1 .. 5 {
var cube = String.empty();
cube <- n * n * n;
cube.padStart(5); // five wide: " 125"
table <- n, cube.view(), "\n";
}
print <- table.view();
It prints:
1 1 2 8 3 27 4 64 5 125
Other methods change a String where it is: replace, insert, truncate, clear, and upperAscii and lowerAscii as above. Each needs a var variable, since it changes the text (every method of String is in the library reference):
var s = String.from("hello world");
s.replace("world", "there"); // hello there
s.insert(0, "oh, "); // oh, hello there
Asking questions of text#
The methods of str read text without changing it, so a literal has them as well as a String. Here they ask about a place and a date, with its stray spaces trimmed off first:
let place = " Ithaca, 1200 BC ".trim(); // Ithaca, 1200 BC
place.len() // 15
place.startsWith("Ith") // true
place.endsWith("BC") // true
place.contains("12") // true
&place[0 ..< 6] // Ithaca: a piece of the text, itself a &str
"apple" < "banana" // true
== asks whether two texts are the same, and < and the other comparisons put them in order by their characters' codes, in which every capital comes before every small letter: "Zebra" < "apple" is true. find says where a piece of text first starts, or that it does not appear at all. It gives back an Option: Some holding the position, or None. An Option is matched just as a Result is, and Enums shows how both are made:
match place.find(",") {
case let at Some { print <- "comma at ", at, "\n"; } // comma at 6
case None { print <- "no comma\n"; }
}
Counting in bytes#
Text in Cone is UTF-8, so text in any language is kept and printed just as it is written. In UTF-8 an English letter takes one byte, but é and ü take two each, and a Japanese character three. Every length and every position in Cone's text counts bytes, which costs nothing to find, since the text need not be read from the start to reach it. When you want characters, charCount() counts them and chars() walks them, one at a time:
let shop = "Café Zürich";
print <- shop.len(), " bytes, ", shop.charCount(), " characters\n"; // 13 bytes, 11 characters
each c in shop.chars() {
print <- "[", c, "]"; // [C][a][f][é][ ][Z][ü][r][i][c][h]
}
Each c is a char, one character, which is how verdict compares its letters. Bytes also explain a small surprise: upperAscii and lowerAscii change only the 26 ASCII letters, which are one byte each, so "Café Zürich" in capitals comes out as CAFé ZüRICH.
Every method of str and String, how text is handed to C as a cstr, and how bytes become text are listed in Text: str, String and cstr.
You can now build text, ask questions of it, walk its characters, and hand it on knowing that it moves. Next come tuples, the simplest way to keep a few values together, and after them the structs that gather values into types of your own, with methods of their own, just as a String has replace and view.