Capitolo 9

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.

In questo capitolo impari

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 = 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. Calcola la media di italiano e di matematica: le stesse sei righe sono copiate due volte. C'è già anche la lista dell'inglese, ma non la usi. Funziona, ma ogni copia è un'occasione di sbagliare: se nella seconda dimentichi toDouble(), la matematica stampa un numero intero e nessuno se ne accorge. Nel secondo esempio la funzione media serve tutte e tre.

Adesso lo stesso programma con una funzione. Il calcolo ha un nome — media — e i voti su cui lavorare diventano un parametro:

Che cosa è cambiato

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.

Quando vale la pena scrivere una funzione

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:

PezzoNell'esempioA che cosa serve
funfun media(...)dice a Kotlin che stai dichiarando una funzione
nomemediail 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: Doubleche cosa consegna a chi la chiama
corpo{ ... return ... }le istruzioni che fanno il lavoro

I parametri possono essere più di uno, e vanno passati nell'ordine in cui sono dichiarati: nel prossimo esempio la funzione vuole prima il nome e poi l'età.

Il viaggio di andata e ritorno

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.

  1. La prima chiamata passa 50.0 a prezzo e 10 a percentuale: lo sconto è 50.0 * 10 / 100.0 = 5.0, quindi stampa 45.0.
  2. La seconda chiamata riusa la stessa funzione con altri numeri: 80.0 * 50 / 100.0 = 40.0, e 80.0 - 40.0 = 40.0.
  3. Il 100.0 scritto con il punto è quello che salva i decimali: con 100 la divisione sarebbe fra interi e il risultato sarebbe sbagliato.
  4. 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 lavoro sta in una riga sola, puoi scrivere il corpo con = invece che con le graffe e return: è lo stesso. Se invece la funzione deve solo stampare, non scrivere il tipo dopo le parentesi: non consegna niente.

Le due forme, una accanto all'altra

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.

Una funzione che non restituisce niente non si può usare in un'espressione

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.

Che cosa stampa questo programma?

fun saluta(nome: String) {
    println("Ciao $nome")
}

fun main() {
    saluta("Anna")
}
Non stampa niente, perché saluta non ha un tipo di ritornoNo: senza tipo di ritorno la funzione non consegna un valore, ma può comunque stampare.
Ciao AnnaEsatto: main chiama saluta, e quella funzione fa il println.
Errore: manca : String dopo i parametriNo: se non restituisci niente, il tipo di ritorno non si scrive. È proprio la forma di saluta nell'esempio sopra.

Una funzione senza tipo di ritorno serve a fare qualcosa (stampare, aggiornare). Una con il tipo serve a consegnare un valore.

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.

Il 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

Qui succedono due cose, una per volta. Prima: media chiama somma. Non riscrivi il ciclo. Poi: formula ha già un valore ("Ciao"). Se non lo passi, vale quello.

Il tipo di un parametro è una dichiarazione, non un controllo

fun somma(numeri: List<Int>) dice che cosa la funzione si aspetta. Nota il it che non c'è: dentro la funzione i parametri hanno un nome, come numeri. it lo hai usato in filter: qui non serve.

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.
Un errore: manca un argomento nella prima chiamataL'errore ci sarebbe se 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

ScriviCosa 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 * ncorpo 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 valoreconsegna 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.sizeuna 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
Come si legge un errore sulle funzioni

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. Scrivi la funzione media sopra main, non dentro. Con i voti 6, 7, 8, 4, 9 le quattro righe sono Voti: 6, 7, 8, 4, 9, Media della classe: 6.80, Sufficienti: 6, 7, 8, 9 e Media dei sufficienti: 7.50.

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.

  1. La funzione riceve la lista come parametro: non sa e non le interessa se i voti sono cinque o cinquanta.
  2. Il ciclo somma gli elementi e return consegna il risultato: senza return la funzione non darebbe niente a chi la chiama.
  3. media(voti) e media(sufficienti) sono due chiamate della stessa funzione: è il vantaggio di scriverla una volta sola.
  4. filter restituisce 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 leggi nome e età e stampa quanto paga la persona. Con Marta e 12 anni la riga è Marta, 12 anni: 5.00 euro. La funzione sta sopra main, non dentro.

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.

  1. Ogni if controlla solo il limite superiore: se siamo al secondo, sappiamo già che l'età non è sotto i 6 anni.
  2. return esce dalla funzione appena trova la fascia giusta: le condizioni successive non vengono nemmeno guardate.
  3. L'ultimo return 4.0 non ha bisogno di if: se il programma è arrivato lì, l'età è per forza 65 o più.
  4. La funzione restituisce un Double e main lo 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. Stampa anche la lista. Le cinque righe sono: Misure: 12, 7, 9, 21, 5, 18, Minimo: 5, Massimo: 21, Media: 12.0 e Sopra la media: 2. Le funzioni stanno sopra main.

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.

  1. minimo e massimo partono da numeri[0]: con la lista vuota darebbero un errore, ma qui la lista ha sempre sei valori.
  2. Le quattro funzioni sono indipendenti: ognuna riceve la lista e restituisce un numero, senza sapere niente delle altre.
  3. sopraMedia chiama media: il conto della media esiste in un punto solo, e se lo correggi lo correggi per tutti.
  4. Nella media il toDouble() è obbligatorio: 72 / 6 fra interi darebbe 12, e va bene solo per caso.

Scrivi da zero il programma del conto di un ristorante. Legge, nell'ordine:

  1. quante voci ci sono nel conto;
  2. per ogni voce: il nome del piatto, la quantità e il prezzo unitario.

Per ogni voce stampa una riga nel formato Pizza x2 = 13.00 euro. Alla fine stampa due righe: Totale: 26.50 euro e Con il servizio: 29.15 euro (il servizio è il 10% in più sul totale). Devono esserci almeno due funzioni: una che calcola il totale di una voce e una che applica il servizio. Con i dati di prova: Pizza 2 × 6.5 = 13, Acqua 1 × 1.5 = 1.5, Tiramisu 3 × 4.0 = 12; somma 26.5; con servizio 29.15.

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(...).

  1. Le due funzioni stanno fuori da main, in cima al programma: è la posizione abituale delle funzioni di servizio.
  2. totale riceve prezzo e quantità nell'ordine dichiarato: invertirli darebbe un risultato sbagliato senza nessun errore, ed è l'errore più frequente del capitolo.
  3. L'accumulatore conto parte da 0.0 perché somma Double: con 0 il primo risultato sarebbe comunque decimale, ma il tipo dichiarato sarebbe fuorviante.
  4. conServizio si applica una volta sola, alla fine, al totale: applicarlo a ogni voce darebbe lo stesso numero qui, ma non nei conti con arrotondamenti.
  5. 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. Alla fine le due righe sono Totale: 7.50 euro e Con lo sconto: 6.75 euro.

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.

  1. Il messaggio Manca l'argomento "costoUnitario" nella chiamata a prezzo(...) dice esattamente che cosa manca: prezzo(quantita, costo).
  2. Il secondo errore non dà un messaggio di sintassi ma di nome: Nome non definito: "scontoTotale". La funzione si chiama sconto.
  3. I due argomenti si passano nell'ordine dei parametri: prima la quantità (Int), poi il costo unitario (Double).
  4. Con i dati 3 e 2.5 il totale è 7.50 euro e lo sconto 6.75.
  • so dichiarare una funzione con fun, parametri e tipo di ritorno
  • so scegliere fra il corpo con = e il corpo con le graffe e return
  • so chiamare una funzione passando gli argomenti nell'ordine giusto
  • so scrivere una funzione che riceve un elenco e restituisce un numero
  • so usare il valore di default di un parametro
  • so leggere un messaggio di errore sulle funzioni e correggere il programma da solo