Le funzioni: scrivere una volta, usare molte
Finora ogni programma è una fila di istruzioni dentro main. Quando lo stesso calcolo
serve in tre punti diversi — la media di tre materie, il prezzo di tre biglietti — copiarlo tre
volte è la strada più corta e la più fragile: basta sbagliare una copia e i risultati non tornano
più. Una funzione dà un nome a un pezzo di calcolo e ti permette di usarlo quante
volte vuoi, scrivendolo una volta sola.
- a dichiarare una funzione con
fun nome(parametri): tipo; - il corpo scritto con
=(una riga) e quello con le graffe ereturn; - a chiamare una funzione, passando gli argomenti nell'ordine giusto;
- le funzioni che non restituiscono niente;
- i parametri con valore di default;
- le funzioni che ricevono un elenco (
List<Int>) e restituiscono un numero.
Ripasso lampo: valori che escono dai comandi
Una funzione assomiglia a quello che già usi: ripasso del capitolo 8 per i testi e del capitolo 6 per gli elenchi.
Che cosa stampa questo programma?
fun main() {
val parola = "manuale"
println(parola.uppercase())
println(parola.length)
}MANUALE e poi 7Esatto: uppercase() consegna una versione maiuscola del testo, length il numero di caratteri.manuale e poi 7No: uppercase() non serve a niente se poi non usi quello che restituisce. Qui il suo risultato finisce dentro println.MANUALE e poi manualeAttenzione: length è un numero (la lunghezza), non le lettere della parola.Molti comandi restituiscono un valore invece di stampare: uppercase() consegna il testo maiuscolo, length consegna un numero. In questo capitolo impari a costruire i tuoi: pezzi di calcolo che consegnano un risultato a chi li chiama.
Che cosa stampa questo programma?
fun main() {
val voti = listOf(6, 8, 10)
var somma = 0
for (v in voti) somma += v
println(somma / voti.size)
}8Esatto: 24 diviso 3 fa 8, e la divisione fra interi dà un intero.24No: 24 è la somma, ma poi si divide per il numero di voti.8.0Attenzione: somma e size sono due Int, quindi il risultato è 8. Per i decimali serve un Double nel conto.Questo è il conto che nel capitolo 6 scrivevi a mano ogni volta: sommare e dividere per la dimensione. Nel capitolo di oggi diventa una funzione con un nome — media(voti) — da riusare senza ricopiare il ciclo.
9.1 Perché le funzioni: lo stesso conto tre volte
Guarda il programma qui sotto. Serve la media di tre materie, e le stesse sei righe compaiono
due volte (nel tuo programma vero due volte più una). Funziona, ma ogni copia è
un'occasione di sbagliare: se nella seconda dimentichi toDouble(), la
matematica stampa un numero intero e nessuno se ne accorge.
Adesso lo stesso programma con una funzione. Il calcolo ha un nome —
media — e i voti su cui lavorare diventano un parametro:
L'output è lo stesso (prova ad aggiungere a mano la riga dell'inglese nel primo esempio e confronta). Nel secondo, però, il calcolo della media esiste una volta sola: oggi tre materie, domani trenta, e le righe da scrivere restano due. Se domani scopri un errore nel conto, lo correggi in un punto solo.
- Lo stesso calcolo ti serve due o più volte: scrivilo una volta e chiamalo.
- Il calcolo ha un nome che si spiega da solo:
media(voti)si legge,somma1/sizeva interpretato. - Un pezzo di
mainè diventato lungo e non si capisce più dove comincia e dove finisce: dagli un nome e portalo fuori.
9.2 Dichiarare una funzione: parametri e tipo di ritorno
Una dichiarazione di funzione si legge come una frase: "c'è una funzione che si chiama
media, riceve un elenco di interi e restituisce un decimale". Ogni pezzo ha il
suo posto:
| Pezzo | Nell'esempio | A che cosa serve |
|---|---|---|
fun | fun media(...) | dice a Kotlin che stai dichiarando una funzione |
| nome | media | il nome con cui la chiamerai: si scrive minuscolo, come le variabili |
| parametri | (voti: List<Int>) | i dati che la funzione riceve dall'esterno: nome e tipo, separati da : |
| tipo di ritorno | : Double | che cosa consegna a chi la chiama |
| corpo | { ... return ... } | le istruzioni che fanno il lavoro |
I parametri si possono essere più d'uno, e vanno passati nell'ordine in cui sono dichiarati: nel prossimo esempio la funzione vuole prima il nome e poi l'età.
Nel visualizzatore sopra trovi due tipi di passo nuovi: Chiamata di funzione, dove vedi
il valore dell'argomento che entra nella funzione, e Ritorno dalla funzione, dove vedi il
risultato che esce. È il viaggio di ogni funzione: main chiama,
doppio lavora sulla sua copia di n e restituisce 2 * n,
poi main riprende da dove si era fermato.
- La prima chiamata passa
50.0aprezzoe10apercentuale: lo sconto è50.0 * 10 / 100.0 = 5.0, quindi stampa45.0. - La seconda chiamata riusa la stessa funzione con altri numeri:
80.0 * 50 / 100.0 = 40.0, e80.0 - 40.0 = 40.0. - Il
100.0scritto con il punto è quello che salva i decimali: con100la divisione sarebbe fra interi e il risultato sarebbe sbagliato. - La stessa funzione, due coppie di valori diversi: è tutto il vantaggio delle funzioni.
9.3 Corpo con = o con le graffe, e funzioni che non restituiscono niente
Se il calcolo sta in una sola espressione, puoi scrivere il corpo con =
invece che con le graffe e return: sono due modi di scrivere la stessa cosa. Se invece
la funzione deve stampare e non consegnare nulla, il tipo di ritorno non si scrive.
fun doppio(n: Int): Int = n * 2 è la forma corta di
fun doppio(n: Int): Int { return n * 2 }. Per un calcolo di una riga si usa il
=; quando servono più istruzioni, come in media, si usano le graffe con
return. Entrambe le forme vanno bene: scegli quella che si legge meglio.
saluta("Anna") va bene come istruzione a sé stante, ma
println(saluta("Anna")) non ha senso: saluta non consegna nessun valore.
Se ti serve il risultato dentro un'altra espressione, la funzione deve avere un tipo di ritorno e
un return.
Questo programma scrive la chiamata prima della definizione. Che cosa succede?
fun main() {
println(cubo(3))
}
fun cubo(n: Int): Int = n * n * n27: l'ordine non contaEsatto: Kotlin legge prima tutte le dichiarazioni di funzioni, quindi cubo è già pronta quando main la chiama.mainSarebbe vero per una variabile, non per una funzione: le dichiarazioni di funzioni si vedono tutte, prima o dopo main.27 e poi il valore di cuboNo: dichiarare una funzione non la esegue. Il suo corpo parte solo quando qualcuno la chiama.Per convenzione le funzioni di servizio si scrivono sopra main, così chi legge trova prima gli strumenti e poi il programma; ma la lingua non lo impone. L'importante è quello che dentro main arriva al momento giusto.
9.4 return: consegnare il risultato e uscire subito
return fa due cose insieme: consegna il valore a chi ha chiamato la funzione e
esce da lì, saltando tutto quello che viene dopo. È il modo più chiaro di scrivere
una funzione con più casi: si mettono le condizioni e si esce dalla prima che vale.
return finale non ha bisogno di if
Se siamo arrivati fino in fondo, vuol dire che nessuna delle condizioni precedenti era vera: il
caso rimasto si scrive con un return diretto, senza condizione. È lo stesso ragionamento
dell'else finale di una catena di if, che non deve ripetere tutte le
condizioni già escluse.
9.5 Funzioni che ricevono elenchi e parametri con valore di default
Il parametro di una funzione può essere un elenco, e una funzione può chiamarne un'altra: qui
media usa somma invece di riscrivere il ciclo. L'ultimo parametro può
avere un valore di default, che vale quando chi chiama non passa niente.
fun somma(numeri: List<Int>) dice che cosa la funzione si aspetta. Nota il
it che non c'è: dentro la funzione numeri è un nome come gli altri,
mentre it compariva solo nelle lambda passate a filter e
map. Una lambda è una funzione senza nome che scrivi in linea; qui invece la
funzione ha un nome e la puoi chiamare da qualunque punto del programma.
Che cosa stampa questo programma?
fun saluta(nome: String, formula: String = "Ciao"): String = "$formula $nome!"
fun main() {
println(saluta("Anna"))
println(saluta("Luca", "Ehi"))
}Ciao Anna! e poi Ehi Luca!Esatto: nella prima chiamata il secondo argomento manca e vale il default "Ciao"; nella seconda il valore passato sostituisce il default.formula non avesse un valore di default. Qui Kotlin lo trova nella dichiarazione e va avanti.Ciao Anna! e poi Ciao Luca!No: il valore passato ha la precedenza sul default, altrimenti il default sarebbe inutile.Il default rende il parametro facoltativo: chi chiama può decidere se usarlo o no. Serve per i casi in cui il valore "normale" è quasi sempre lo stesso, e lo si vuole cambiare solo ogni tanto.
9.6 Riepilogo
| Scrivi | Cosa fa |
|---|---|
fun media(voti: List<Int>): Double { ... } | dichiara una funzione: nome, parametri fra parentesi, tipo di ritorno dopo i due punti |
fun quadrato(n: Int): Int = n * n | corpo a espressione: forma corta di { return n * n } |
fun saluta(nome: String) { println(...) } | funzione che non restituisce niente: il tipo di ritorno non si scrive |
return valore | consegna il risultato a chi ha chiamato e esce subito dalla funzione |
media(voti) | chiama la funzione passando gli argomenti nell'ordine dei parametri |
fun saluta(nome: String, formula: String = "Ciao") | parametro con valore di default: si può omettere nella chiamata |
fun media(numeri: List<Int>): Double = somma(numeri).toDouble() / numeri.size | una funzione può chiamarne un'altra e usarne il risultato |
println(cubo(3)) | il risultato di una funzione si usa dentro un'espressione, come una variabile |
- Manca l'argomento "x" nella chiamata a f(...) → hai dimenticato un valore fra le parentesi.
- Troppi argomenti per f → ne hai passati più di quanti la funzione ne dichiara.
- La funzione f non ha il parametro "x" → hai scritto
f(nome = ...)con un nome di parametro che non esiste. - Nome non definito: "f" → hai chiamato una funzione che non hai mai dichiarato, oppure l'hai scritta con un altro nome.
9.7 Esercizi guidati
Leggi 5 voti in una lista. Poi stampa: i voti letti, la media della classe, l'elenco dei soli sufficienti e la media dei sufficienti. Il calcolo della media va scritto in una funzione, perché serve due volte.
Il conto della media è quello del ripasso: accumula in var somma, poi
somma.toDouble() / voti.size (il toDouble() è quello che tiene i
decimali). I sufficienti si prendono con voti.filter { it >= 6 }, e il risultato
è una lista che puoi passare di nuovo alla stessa funzione.
- La funzione riceve la lista come parametro: non sa e non le interessa se i voti sono cinque o cinquanta.
- Il ciclo somma gli elementi e
returnconsegna il risultato: senzareturnla funzione non darebbe niente a chi la chiama. media(voti)emedia(sufficienti)sono due chiamate della stessa funzione: è il vantaggio di scriverla una volta sola.filterrestituisce una lista, quindi si può passare direttamente come argomento: funzioni e metodi delle collezioni si combinano senza problemi.
Il museo applica queste tariffe: fino a 5 anni gratis, da 6 a 17 anni 5 euro, da 18 a 64 anni
12 euro, dai 65 anni in su 4 euro. Scrivi una funzione prezzo(eta: Int): Double che
restituisce il prezzo giusto, poi leggili nome e età e stampa quanto paga la persona.
Dentro la funzione non serve una catena di else if: usa un return per
ogni fascia, dalla più stretta alla più larga, e lascia il caso dei più grandi come
return finale senza if. I prezzi sono Double: scrivili
0.0, 5.0, 12.0, 4.0.
- Ogni
ifcontrolla solo il limite superiore: se siamo al secondo, sappiamo già che l'età non è sotto i 6 anni. returnesce dalla funzione appena trova la fascia giusta: le condizioni successive non vengono nemmeno guardate.- L'ultimo
return 4.0non ha bisogno diif: se il programma è arrivato lì, l'età è per forza 65 o più. - La funzione restituisce un
Doubleemainlo usa dentro"%.2f".format(...): il risultato di una funzione si usa come qualsiasi altro valore.
Data la lista 12, 7, 9, 21, 5, 18, scrivi quattro funzioni — minimo,
massimo, media e sopraMedia — e stampa i quattro
risultati. sopraMedia deve dire quanti valori sono più grandi della media, e deve
usare la funzione media invece di ricalcolarla.
Per minimo e massimo parti dal primo elemento come riferimento e confronta gli
altri, come nello schema del capitolo 6: partire da 0 darebbe risultati sbagliati con numeri
tutti negativi. Il tipo di ritorno è Int per minimo, massimo e sopraMedia,
Double per media.
minimoemassimopartono danumeri[0]: con la lista vuota darebbero un errore, ma qui la lista ha sempre sei valori.- Le quattro funzioni sono indipendenti: ognuna riceve la lista e restituisce un numero, senza sapere niente delle altre.
sopraMediachiamamedia: il conto della media esiste in un punto solo, e se lo correggi lo correggi per tutti.- Nella media il
toDouble()è obbligatorio:72 / 6fra interi darebbe12, e va bene solo per caso.
Scrivi da zero il programma del conto di un ristorante. Legge, nell'ordine:
- quante voci ci sono nel conto;
- per ogni voce: il nome del piatto, la quantità e il prezzo unitario.
Per ogni voce stampa una riga con nome, quantità e totale della voce. Alla fine stampa il
totale del conto e il totale con il servizio del 10% in più. Devono esserci almeno due
funzioni: una che calcola il totale di una voce e una che applica il servizio.
Con i dati di prova l'ultima riga vale 29.15 euro.
Il totale di una voce è prezzo * quantita; il servizio è importo * 1.1.
Accumula il conto in un var conto = 0.0 che parte da zero e cresce a ogni giro:
è l'accumulatore del capitolo 5, questa volta con i decimali. Per i prezzi usa
"%.2f".format(...).
- Le due funzioni stanno fuori da
main, in cima al programma: è la posizione abituale delle funzioni di servizio. totalericeve prezzo e quantità nell'ordine dichiarato: invertirli darebbe un risultato sbagliato senza nessun errore, ed è l'errore più frequente del capitolo.- L'accumulatore
contoparte da0.0perché sommaDouble: con0il primo risultato sarebbe comunque decimale, ma il tipo dichiarato sarebbe fuorviante. conServiziosi applica una volta sola, alla fine, al totale: applicarlo a ogni voce darebbe lo stesso numero qui, ma non nei conti con arrotondamenti.- Le letture usano la forma robusta
readlnOrNull()?.toDoubleOrNull() ?: 0.0: un dato mancante o sporco non ferma il programma.
Questo programma deve stampare il totale di un acquisto e il totale scontato del 10%, ma non parte. Contiene due errori: uno riguarda una chiamata, l'altro un nome. Correggili uno alla volta e confronta il risultato con la soluzione.
Il primo errore è dentro la chiamata a prezzo: la funzione dichiara
due parametri e ne stai passando uno solo. Il secondo è il nome della funzione
chiamata nella seconda stampa: esiste una funzione dichiarata che fa esattamente quel lavoro,
ma si chiama in un altro modo.
- Il messaggio Manca l'argomento "costoUnitario" nella chiamata a prezzo(...) dice esattamente che cosa manca:
prezzo(quantita, costo). - Il secondo errore non dà un messaggio di sintassi ma di nome: Nome non definito: "scontoTotale". La funzione si chiama
sconto. - I due argomenti si passano nell'ordine dei parametri: prima la quantità (
Int), poi il costo unitario (Double). - Con i dati
3e2.5il totale è7.50euro e lo sconto6.75.
fun, parametri e tipo di ritorno= e il corpo con le graffe e return