Heroes / Guida / capitolo 9

Le stringhe

Leggere, dividere e unire stringhe, distinguere byte e caratteri, e mettere un valore dentro una stringa.

Uno str contiene testo UTF-8, una codifica in cui un carattere può occupare più byte. Una stringa non cambia dopo la creazione. len() misura la lunghezza in byte e anche gli indici di slice si riferiscono ai byte.

L’indice ti dà un 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
examples/gallery/07-strings.hero, righe 7–15

s[i] ti dà un u8, cioè un numero da 0 a 255, perché un byte è quello. Viene confrontato con ' ' fra apici singoli, che è il modo di scrivere un carattere singolo e vale anche lui come numero. Con le virgolette doppie avresti una stringa, e le due cose non si mescolano:

$ heroes check strings.hero
error[type_mismatch]: expected `u8`, found `str`
  at strings.hero:12:12
     |
  12 |         if s[i] == " "
     |            ^^^^^^^^^^^

Heroes usa tipi diversi per numeri e stringhe e non li converte automaticamente. Il confronto dell’esempio è quindi un errore di compilazione.

chars() ti dà i caratteri

# 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
examples/gallery/07-strings.hero, righe 17–25

chars() percorre caratteri interi e te li restituisce uno per uno come str di un carattere: ecco perché questa funzione confronta con " " fra virgolette doppie mentre quella di prima usava ' '. Il ciclo sui byte va bene per un protocollo o per uno scanner; chars() va bene per qualunque cosa leggerà una persona. La sintassi rende esplicita la scelta tra byte e caratteri.

Se una porzione di stringa taglia un carattere, il programma si ferma e segnala l’errore. In questo modo non produce testo UTF-8 non valido.

Dividere il testo in parole

# 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
examples/gallery/07-strings.hero, righe 27–40

Heroes non include una funzione split. Questo esempio divide il testo in corrispondenza degli spazi. Il controllo if i > start evita di aggiungere parole vuote quando due spazi sono consecutivi.

slice(from:, to:) lavora in byte, con to escluso, e nomina tutti e due gli estremi perché sono dello stesso tipo, la stessa regola per cui range nomina i suoi.

Rimetterle insieme

# `join` is the other direction, and the separator is an argument, not a
# convention.
function sentence_of(words: [str]) -> str
    return words.join(" ")
examples/gallery/07-strings.hero, righe 42–45
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
examples/gallery/07-strings.hero, righe 47–54

join costruisce il risultato in una sola passata. Il ciclo di concatenazione crea invece una nuova stringa a ogni iterazione e ricopia anche il testo già accumulato. La differenza diventa rilevante con stringhe lunghe. join e repeat evitano queste copie ripetute.

Testo con dei buchi dentro

Una stringa può portare dei valori. Una f prima della virgoletta d’apertura trasforma {n} nel testo di n, scritto come lo scriverebbe to_str. In un buco può stare qualsiasi espressione, e il buco finisce alla graffa che lo chiude, così una chiamata o una ricerca al suo interno si legge come si leggerebbe ovunque. {{ e }} scrivono ciascuna una graffa, e una } da sola è un errore: una graffa intesa come testo si raddoppia sempre. Una stringa senza la f tiene le sue graffe come testo, tranne dove formerebbero un buco che nomina solo ciò che esiste in quel punto, come in "{i + 1}": lì è rifiutata come una f dimenticata, e le graffe intese come testo si scrivono allora {{ e }} in una stringa con la f.

function line(at: i64, of: i64, star: Star) -> str
    return f"{at}/{of} {star.name} at magnitude {star.magnitude}"
examples/gallery/12-interpolation.hero, righe 20–21

In un buco può stare ciò che to_str sa rendere: un intero, un numero con la virgola, un bool o una str. Un record o un array non hanno un testo proprio, e un valore che potrebbe mancare va prima reso certo con .must(). Il compilatore rifiuta il buco e la sua nota dice cosa scrivere al suo posto. L’esempio intero →

$ 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

Gli escape disponibili

Dentro una stringa: \n, \t, \r, \\, \". Dentro un letterale di carattere, \' prende il posto dell’ultimo. Un escape diverso produce un errore e un suggerimento di correzione:

$ 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 segnala gli escape sconosciuti, cioè le sequenze con una barra inversa che non riconosce. Per inserire una barra inversa letterale devi raddoppiarla. Nell’esempio il compilatore suggerisce \\d e può applicare la correzione automaticamente.

Il programma

function main()
    words = words_of("the quick brown fox")
    print(sentence_of(words))
    print(initials_of(words))
examples/gallery/07-strings.hero, righe 63–66
$ heroes run examples/gallery/07-strings.hero
the quick brown fox
tqbf

Poi: Le mappe, dove cercare una chiave ti restituisce un valore oppure un errore, e non c’è nessun null da dimenticare. Torna all’elenco dei capitoli. ← Prima: Generici e valori funzione

Esegui un programma tuo: bastano quattro comandi. Oppure prendi tutto il linguaggio in un file solo, e tutti i programmi già scritti.