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 += 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 nomemedia — 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 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à.

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

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.

Questo programma scrive la chiamata prima della definizione. Che cosa succede?

fun main() {
    println(cubo(3))
}

fun cubo(n: Int): Int = n * n * n
Stampa 27: l'ordine non contaEsatto: Kotlin legge prima tutte le dichiarazioni di funzioni, quindi cubo è già pronta quando main la chiama.
Errore: la funzione va scritta prima di mainSarebbe vero per una variabile, non per una funzione: le dichiarazioni di funzioni si vedono tutte, prima o dopo main.
Stampa 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.

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

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.

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

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

  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.

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

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

  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