Strings & text
Building and inspecting text.
Joining
Section titled “Joining”The + operator concatenates strings, and println /
f-strings join for you:
let first = "Ada";let last = "Lovelace";
let full = first + " " + last;let hi = f"hello, {first}";println("full name: ", full);to_string(x) converts a number, bool, duration, etc. into its
text form when you need a String specifically:
let n = 42;let label = "n=" + to_string(n);Length and inspection
Section titled “Length and inspection”len(s) is a builtin — the byte length of the string:
let s = "hello";println(len(s)); // 5Most text operations live in std::str, called as plain
functions:
let i = std::str::index_of("hello world", "world"); // 6let sub = std::str::substring("hello world", 0, 5); // "hello"let up = std::str::upper("hi"); // "HI"let t = std::str::trim(" spaced "); // "spaced"let r = std::str::replace("a-b-c", "-", "+"); // "a+b+c"Hale has no per-character method syntax (s.charAt(i)); you
slice with a range or use the std::str helpers. Slicing a
string by byte range:
let s = "hello";let h = s[0..1]; // "h"Parsing numbers
Section titled “Parsing numbers”Turning text into a number can fail — the text might not be a number. So the parse functions are fallible, and the next chapter (When a call can fail) is exactly about how you handle that. The shape, previewed:
let n = std::str::parse_int("42") or 0; // 42, or 0 if it wasn'tThere are also non-failing predicates to check first
(std::str::can_parse_int) when you’d rather branch than
recover.
Text is String; raw binary is Bytes. They’re different types
because they have different rules — a String is valid UTF-8, a
Bytes is any sequence of octets, including embedded zeros.
let b = std::bytes::from_string("hello"); // String -> Byteslet s = std::str::from_bytes(b); // Bytes -> Stringlet byte0 = std::bytes::at(b, 0) or 0; // a single byte (fallible)You’ll work with Bytes directly when you read from a socket or
a file and need to frame messages yourself — that’s a topic for
wire formats and the systems tier. At
this level, just know the two types are distinct and you convert
explicitly between them.
Next: the failure model — When a call can fail.
Splitting, joining, searching
Section titled “Splitting, joining, searching”std::str::contains(line, "=") // Boolstd::str::starts_with(line, "#")std::str::ends_with(path, ".hl")Splitting writes into a collection you supply rather than returning a new one:
@form(vec)locus Fields { capacity { heap items of String; } }
let f = Fields { };std::str::split_into("a,b,c", ",", f);println(std::str::join(f, " | ")); // a | b | cThe asymmetry is deliberate, and it’s worth understanding rather than
memorising. join returns a String because a String is already a
value. split cannot return a list, because Hale has no list value
to return — collections are loci you own. So you hand it the storage.
That turns out to be the better shape anyway: you own the allocation, so it’s counted against your budget instead of hidden inside a return value, and a handler that splits a line per message can reuse one collection instead of allocating a fresh one every time.
Empty fields survive: "a,,b," splits into four, not two. Losing them
silently would drop a column from a CSV row and you would never see it
happen.
Text that isn’t ASCII
Section titled “Text that isn’t ASCII”String is bytes. When you need characters, ask for code points
explicitly:
std::str::cp_count("héllo") // 5, though the string is 6 bytesstd::str::cp_at("日本語", 0) // 26085std::str::cp_size("héllo", 1) // 2 — 'é' is two bytesInvalid UTF-8 gives you -1, as does a byte offset that lands in the
middle of a character. It does not quietly hand back a replacement
character, because then you could not tell a corrupted string from one
that genuinely contains that character.
Normalization, case folding beyond ASCII, and grapheme clusters are
not provided. Each is a large commitment with its own tables, and a
to_upper that works for English and mangles Turkish is worse than
one that isn’t there.
Patterns
Section titled “Patterns”std::regex::matches("h.llo", "hello") // true — a full matchstd::regex::find("l+", "hello") // 2 — leftmost byte offsetstd::regex::valid("a(") // falseLiterals, ., *, +, ?, |, grouping, and character classes
([a-z], [^a-z]). No backreferences and no lookahead — those need a
backtracking engine, and a backtracking match has no upper bound on
how long it can take. In a language where you can write
@budget(alloc_per_call = 0) on a handler, a pattern that might take
exponential time is not something you can be allowed to put there.
Check valid on any pattern you didn’t write yourself. Without it, a
typo looks exactly like “no matches”.