Anatomia di una funzione
La forma generale di una funzione in Zero:
fun name(param1: Type1, param2: Type2) -> ReturnType {
// corpo
return value
}
Le parti:
fun: la parola chiave che introduce una funzione.name: il nome della funzione.(param1: Type1, ...): l'elenco dei parametri. Ogni parametro ha un tipo esplicito.-> ReturnType: il tipo di ritorno.{ ... }: il corpo, un blocco di istruzioni.return value: esce dalla funzione convalue.
Un piccolo esempio concreto:
fun double(value: i32) -> i32 {
return value * 2
}
Questa è la funzione intera. Riceve un i32, ne restituisce un altro e non fa I/O. All'interno del corpo, value è un binding in stile let: puoi usarlo come qualsiasi altra variabile locale.
Chiamare le funzioni
Le chiamate hanno l'aspetto che ti aspetti:
let result = double(21)
L'argomento deve corrispondere al tipo del parametro. Il risultato viene associato a result, che il compilatore deduce essere i32 perché double restituisce i32.
Un esempio completo che mette insieme una funzione di supporto e un main: clicca su Esegui per vederlo in azione:
Dovresti ottenere math works\n sullo stdout.
pub e visibilità
Per impostazione predefinita, una funzione dichiarata in un file è privata di quel file (o modulo: le regole di visibilità diventano più precise man mano che i progetti crescono). Per esporre una funzione fuori dal suo modulo, anteponi pub:
pub fun greet() -> String {
return "ciao\n"
}
Senza pub, il codice di altri moduli non può chiamare greet. Il runtime deve chiamare main dall'esterno di qualsiasi modulo definito dall'utente, ed è per questo che main è sempre pub.
La regola del privato per impostazione predefinita è una buona abitudine da seguire. Segna solo ciò che intendi come interfaccia; il resto rimane dentro il modulo.
Tipi di ritorno
Ogni funzione dichiara il suo tipo di ritorno dopo ->. Tipi di ritorno comuni:
fun answer() -> i32 { return 42 }
fun ok() -> bool { return true }
fun label() -> String { return "ready\n" }
fun nothing() -> Void { }
Void è il tipo di ritorno di una funzione che fa il suo lavoro tramite effetti collaterali invece di produrre un valore. Una funzione Void non ha bisogno di un return esplicito: basta arrivare alla fine del corpo.
fun log(world: World, message: String) -> Void raises {
check world.out.write(message)
}
Chiamate a funzione che scartano un valore
Se una funzione restituisce un valore e non ti interessa, devi comunque prendere atto del valore restituito. L'idioma è associarlo con let:
ignored è un binding che il resto della funzione non legge mai. La convenzione di usare il nome ignored (o _) segnala che lo scarto è intenzionale. È più scomodo che lasciar cadere in silenzio un valore di ritorno, ed è proprio questo il punto: in un linguaggio in cui gli agenti leggono e generano codice, un valore non letto è spesso un bug che vale la pena far emergere.
Il ruolo di raises
Una funzione che potrebbe fallire lo dichiara nella firma. L'abbiamo visto su main:
pub fun main(world: World) -> Void raises {
check world.out.write("ciao\n")
}
La clausola raises può essere semplice (qualsiasi errore) o specifica:
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
raises { InvalidInput } significa "questa funzione può fallire con InvalidInput e con nient'altro". Chi la chiama deve usare check (o una forma di gestione più elaborata) per propagare o gestire l'errore.
Raises e check approfondisce l'argomento, compreso cosa succede con più tipi di errore e come check interagisce con la clausola raises del chiamante.
Funzioni generiche
Quando vuoi che una funzione lavori con più di un tipo, dichiara dei parametri di tipo tra parentesi angolari:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T e U sono parametri di tipo: è chi chiama a decidere cosa sono. Chiamare makePair(40, 2_u8) ti dà un Pair<i32, u8>. Leggi Generics per tutti i dettagli, comprese le shape generiche e i vincoli.
Dove vivono le funzioni
In un programma piccolo scrivi le funzioni direttamente nel tuo file .0. In un pacchetto distribuisci le funzioni su più file in src/ e il compilatore risolve per te i riferimenti tra file. Le basi restano le stesse, cioè fun, parametri, tipo di ritorno e corpo, indipendentemente da dove si trovi fisicamente la funzione.
Note di stile
Alcune convenzioni che vedrai negli esempi ufficiali:
- Nomi di funzione in minuscolo, con le parole unite (
makePair) o separate in camelCase. La libreria standard tende al camelCase. - Un solo valore di ritorno per funzione. Se devi restituire più cose, crea una piccola
shapeper contenerle: è più chiaro che restituire una tupla di coppie di tuple. - Le funzioni
Voidfanno solo chiamatecheck; le funzioni che calcolano un valore evitano l'I/O quando possono. Questa separazione è in parte culturale e in parte imposta: una funzione di calcolo puro non riceveworlde quindi non può letteralmente fare I/O.
L'ultimo punto merita un approfondimento. Dato che l'I/O passa dalla capability World e World viene passato esplicitamente, la firma di una funzione ti dice se potrebbe fare I/O. Le funzioni la cui firma non menziona World sono pure rispetto al mondo esterno. È una proprietà su cui gli agenti (e le persone) possono contare senza leggere il corpo.
Prossimo passo: if/else
Hai già visto comparire if di sfuggita: la prossima pagina tratta nel dettaglio le espressioni if/else, compreso il modo in cui interagiscono con i binding e cosa manca di proposito (niente coercizione truthy, niente operatore ternario).
Domande frequenti
Come si dichiara una funzione in Zero?
Usa fun: fun name(param: Type) -> ReturnType { body }. Aggiungi pub davanti per rendere la funzione visibile fuori dal suo modulo. Aggiungi raises dopo il tipo di ritorno se la funzione può fallire. Per esempio: pub fun double(value: i32) -> i32 { return value * 2 }.
Cosa fa la parola chiave pub?
pub?pub segna una dichiarazione come pubblica, cioè visibile al codice fuori dal suo modulo. Senza pub, una funzione è privata del file (o del pacchetto) in cui è dichiarata. Il punto di ingresso convenzionale pub fun main deve essere pubblico perché il runtime possa trovarlo e chiamarlo.
Come si restituisce un valore da una funzione in Zero?
Scrivi return value nel corpo della funzione. L'espressione deve corrispondere al tipo di ritorno dichiarato. Una funzione con tipo di ritorno Void non restituisce nulla e non ha bisogno di un'istruzione return esplicita: arrivare alla fine del corpo va bene.
Le funzioni di Zero possono ricevere più parametri?
Sì. Elencali tra parentesi separati da virgole, ciascuno con nome e tipo: fun add(a: i32, b: i32) -> i32 { return a + b }. Ogni parametro è un binding in stile let nel corpo della funzione. Zero richiede tipi espliciti sui parametri: nelle dichiarazioni di funzione non c'è inferenza del tipo dei parametri.
Cosa significa raises nella firma di una funzione?
raises nella firma di una funzione?raises dichiara che la funzione può fallire. Un raises semplice ammette qualsiasi tipo di errore; raises { InvalidInput } lo limita a un errore specifico con nome. Chi chiama la funzione deve usare check (o un altro costrutto per gli errori) per prendere atto della possibilità di fallimento: non può ignorarla in silenzio.