Strings
Reading, splitting and joining strings, distinguishing bytes from characters, and putting a value inside one.
A str contains UTF-8 text, an encoding in which a character can occupy multiple bytes. A string does not change after creation. len() measures length in bytes, and slice indices also refer to bytes.
Indexing gives you a byte
# Bytes, not characters: this counts the ASCII spaces.
function space_count(s: str) -> i64
seen: i64 @= 0
for i in range(from: 0, to: s.len())
if s[i] == ' '
seen @ seen + 1
return seen
s[i] gives you a u8, a number from 0 to 255,
because that is what a byte is. It is compared against ' ' in
single quotes, which is how a single character is written, and a character is a number too. With double quotes you would have a string, and the two do not
mix:
$ heroes check strings.hero
error[type_mismatch]: expected `u8`, found `str`
at strings.hero:12:12
|
12 | if s[i] == " "
| ^^^^^^^^^^^
Heroes uses different types for numbers and strings and does not convert between them automatically. The comparison in the example is therefore a compile error.
chars() gives you characters
# Characters, not bytes: this one is right for text.
function letters_of(s: str) -> [str]
out: [str] @= []
for c in s.chars()
if c != " "
out @ out.push(c)
return out
chars() walks whole characters and hands each one back as a
one-character str, which is why this function compares against
" " in double quotes while the previous one used
' '. Use the byte loop for a protocol or a scanner; use
chars() for anything a person will read.
Slicing through a character stops the program and reports an error. This prevents the operation from producing invalid UTF-8 text.
Splitting text into words
# Splitting on a single space, written out. There is no library to hide in.
function words_of(s: str) -> [str]
out: [str] @= []
start: i64 @= 0
for i in range(from: 0, to: s.len())
if s[i] == ' '
if i > start
out @ out.push(s.slice(from: start, to: i))
start @ i + 1
if s.len() > start
out @ out.push(s.slice(from: start, to: s.len()))
return out
Heroes has no built-in split function. This example splits text at spaces. The if i > start check avoids adding empty words when two spaces appear in a row.
slice(from:, to:) takes bytes, to excluded, and
names both because they are the same type, the same rule that makes
range name its ends.
Putting them back together
# `join` is the other direction, and the separator is an argument, not a
# convention.
function sentence_of(words: [str]) -> str
return words.join(" ")
function initials_of(words: [str]) -> str
out: str @= ""
for w in words
if w.len() > 0
out @ out + w.slice(from: 0, to: 1)
return out
join builds the result in one pass. The concatenation loop creates a new string on each iteration and copies the text already accumulated. The difference matters for long strings. join and repeat avoid these repeated copies.
Text with holes in it
A string can carry values. An f before the opening quote turns {n} into the text of n, written the way to_str would write it. Any expression may stand in a hole, and the hole ends at the brace that closes it, so a call or a lookup inside it reads as it would anywhere. {{ and }} each write one brace, and a lone } is an error, so a brace meant as text is always doubled. A string without the f keeps its braces as text, except where they would make a hole naming only what is in scope there, as in "{i + 1}": that is refused as a forgotten f, and braces meant as text are then written {{ and }} in an f string.
function line(at: i64, of: i64, star: Star) -> str
return f"{at}/{of} {star.name} at magnitude {star.magnitude}"
A hole may hold what to_str can render: an integer, a float, bool or str. A record or an array has no text of its own, and a value that may be missing has to be made whole with .must() first. The compiler refuses the hole and its note says what to write instead. The whole example →
$ heroes run examples/gallery/12-interpolation.hero
1/2 Sirius at magnitude -1.46
2/2 Vega at magnitude 0.03
seen Sirius 2 times, {like this}
nested: 2 stars
Available escapes
In a string: \n, \t, \r,
\\, \". In a character literal, \'
replaces the last of them. Other escape sequences produce an error and a suggested fix:
$ heroes check strings.hero
error[unknown_escape]: `\d` is not an escape sequence — the escapes are \n \t \r \\ \"; write `\\` for a literal backslash
at strings.hero:45:24
|
45 | return words.join("\d")
| ^^
fix (certain): escape the backslash: `\\d`
Heroes reports unknown escapes, backslash sequences it does not recognise. To include a literal backslash, double it. In this example the compiler suggests \\d and can apply the fix automatically.
The program
function main()
words = words_of("the quick brown fox")
print(sentence_of(words))
print(initials_of(words))
$ heroes run examples/gallery/07-strings.hero
the quick brown fox
tqbf
Next: Maps, where looking a key up hands you a value or an error, and there is no null to forget. Back to the chapter list. ← Previous: Generics and function values
Run a program of your own: it takes four commands. Or take the whole language in one file, and every program already written in it.