15. Characters and code points

The character literal 'a' has type Char: one Unicode scalar value, represented as its code point. It is an opaque type over Int (§2.7), so ==, <, hashing and a literal pattern in a match are all Int's — but it is not an Int, 'a' + 1 does not typecheck, and converting between the two goes through std/char: char.code(c) gives the code point, char.of(n) builds a character from one (None if it is not a scalar value).

use std/char
use std/str

fn is_digit(c: Char) -> Bool = c >= '0' && c <= '9'

pub fn main() -> Unit !io = {
  println(to_string(is_digit('7')))
  println(to_string(char.code('a')))
  println(to_string(str.len("héllo 🙂")))
  println(str.slice("世界你好", 0, 2))
  println(from_code_points(['h', 'i']))
}
Open in playground
true
97
7
世界
hi

code_points/from_code_points go back and forth between a string and a List[Char] (supplementary-plane emoji included), str.len counts code points, str.slice slices by code-point index, str.at takes one Char, and str.from_char turns one Char into a string. "${c}" is that same one-character string: std/char writes an impl Display[Char], and Display is the top-level rendering. Show, the nested one, is still the target type's, so a Char inside a list prints as its code-point number.

A function that indexes by code point counts from the front of the string every time (O(n) once, O(n²) inside a loop). To scan a string, use std/cursor: a cursor is an opaque position with a constant cost per step; arithmetic on one is a compile error, while comparing two (==, <) is allowed.

use std/cursor

pub fn main() -> Unit !io = {
  let s = "a🎈b"
  let c = cursor.next(s, cursor.start(s))
  println("${cursor.char(s, c)}")
  println(cursor.slice(s, c, cursor.end(s)))
}
Open in playground
127880
🎈b

One step is one character: an emoji's surrogate pair is never split down the middle. cursor.char answers an Int rather than a Char, because at the end it has to answer -1 — a sentinel that is not a character has no home in a type where every value is one (spec §4.8). cursor.find(s, sub, from) returns an Option[Cursor], and cursor.skip(s, c, sub) steps over a literal already known to occur there.