Repository navigation
Libro 08 Funzioni
Le funzioni sono il modo in cui un programma dà un nome a un'idea e la riusa. In Triad sono anche valori di prima classe, ma questo aspetto è materia del capitolo 9; qui ci concentriamo su come si definiscono, come si chiamano e su una tecnica che in Core è fondamentale: la ricorsione.
La forma breve è adatta alle funzioni che si riducono a un'espressione:
quadrato(x) = x * x
ipotenusa2(a, b) = quadrato(a) + quadrato(b)
println(ipotenusa2(3, 4))Output:
25
La forma a blocco ammette un corpo di più istruzioni; il valore restituito è quello dell'ultima:
Calcoliamo, per esempio, la media intera di tre numeri. In un altro linguaggio si scriverebbe
(a + b + c) / 3, ma Core non ha /: il quoziente si calcola con un ciclo di sottrazioni. È un
buon promemoria di che cosa significhi programmare con un vocabolario ridotto, e di come le
funzioni permettano di costruirsi il vocabolario mancante:
function quoziente(a, b)
q = 0
while a >= b
a -= b
q += 1
end
q
end
function media_intera(a, b, c)
quoziente(a + b + c, 3)
end
println(media_intera(4, 8, 10))Output:
7
Esiste anche una terza forma, la funzione anonima function (a, b) … end, che si assegna a
una variabile come qualunque valore: moltiplica = function (a, b) a * b end.
Una funzione si chiama scrivendo il suo nome seguito dagli argomenti tra parentesi, senza spazio prima della parentesi. Il numero di argomenti deve coincidere con quello dei parametri, altrimenti l'esecuzione si ferma con un errore di arità. Non esistono parametri facoltativi né con nome in Core.
Dentro la funzione, i parametri sono variabili locali, inizializzate con i valori degli
argomenti. Assegnare un parametro (come fa quoziente con a -= b) modifica la copia locale,
non la variabile del chiamante: gli interi, le stringhe e gli altri valori semplici vengono
passati "per valore". Le liste fanno eccezione, perché vengono passate per riferimento: il
capitolo 10 spiega le conseguenze.
return espressione termina la funzione subito, restituendo quel valore; return da solo
restituisce nothing. Serve soprattutto per le uscite anticipate, quando un caso
particolare si risolve prima di arrivare in fondo:
function primo_negativo(xs)
i = 1
while i <= length(xs)
if xs[i] < 0
return i
end
i += 1
end
nothing
end
println(primo_negativo([3, 5, -2, 8]), " ", primo_negativo([1, 2]))Output:
3 nothing
Una funzione esiste dal momento in cui la sua definizione viene eseguita. Chiamarla più in alto
nel file è un errore (undefined variable). Una funzione può però chiamare se stessa nel
proprio corpo, perché quando il corpo viene eseguito la definizione è già avvenuta: è questo
che rende possibile la ricorsione.
Una funzione è ricorsiva quando, per risolvere un problema, si chiama su un problema più piccolo dello stesso tipo. Ogni funzione ricorsiva ha due ingredienti:
- un caso base, abbastanza semplice da risolversi direttamente;
- un caso ricorsivo, che riduce il problema e si fida che la chiamata su quello più piccolo dia la risposta giusta.
fattoriale(n) = n < 2 ? 1 : n * fattoriale(n - 1)
fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
mcd(a, b) = b == 0 ? a : mcd(b, a % b)
println(fattoriale(10), " ", fib(20), " ", mcd(1071, 462))Output:
3628800 6765 21
Prendiamo mcd, l'algoritmo di Euclide. Il caso base è b == 0: il massimo comun divisore di
a e 0 è a. Il caso ricorsivo si basa su un fatto matematico: il MCD di a e b è uguale
al MCD di b e del resto di a diviso b. Ogni chiamata riduce il secondo argomento, che
prima o poi arriva a zero.
fib, invece, mostra un rischio: ogni chiamata ne genera due, e il numero totale di chiamate
cresce esponenzialmente con n. fib(20) è ancora istantaneo, fib(35) sarebbe molto lento.
Quando la ricorsione rifà più volte lo stesso lavoro, conviene trasformarla in un ciclo:
function fib_veloce(n)
a = 0
b = 1
while n > 0
t = a + b
a = b
b = t
n -= 1
end
a
end
println(fib_veloce(90))Output:
2880067194370816120
Ricorsione e profondità. Ogni chiamata ricorsiva occupa memoria finché non termina. Per ricorsioni molto profonde (migliaia di livelli) un ciclo è più sicuro. Triad-Full promette di trasformare la ricorsione infinita in un
Err(:budget)invece che in un crash; in Core vale la regola del buon senso.
In Triad-Full una funzione che stampa deve dichiararlo con !io dopo i parametri. Core accetta
l'annotazione e la ignora: scriverla fin da ora rende il codice pronto per Full e documenta
l'intenzione.
function saluta(nome) !io
println("ciao, " * nome)
end
saluta("mondo")Output:
ciao, mondo
- Forma breve
f(x) = e, forma a bloccofunction f(x) … end, anonimafunction (x) … end. - Il valore di una funzione è l'ultima espressione eseguita;
returnserve per uscire prima. - Gli argomenti devono essere esattamente quanti i parametri; una funzione esiste dopo la sua definizione.
- La ricorsione richiede un caso base e un caso che riduca il problema; se ripete lavoro, meglio un ciclo.
- Scrivi
potenza(b, e)ricorsiva senza usare^. - Scrivi
cifre(n)che conti le cifre decimali di un intero positivo (usa%e un ciclo per dividere per 10). - Scrivi
hanoi(n, da, a, via)che stampi le mosse della torre di Hanoi conndischi.
← Capitolo 7 — Decidere e ripetere · Sommario · Capitolo 9 — Lambda, chiusure e visibilità dei nomi →
Parte prima
Parte seconda 4. Le parole del linguaggio 5. Valori e tipi 6. Espressioni, operatori e variabili 7. Decidere e ripetere 8. Funzioni e ricorsione 9. Lambda, chiusure e visibilità dei nomi 10. Liste e dati strutturati 11. Stringhe, simboli e byte 12. Quando le cose vanno storte 13. Progetto: un piccolo interprete
Parte terza 14. Tipi, struct e multiple dispatch 15. Programmazione logica 16. Codice come dato e macro 17. Garanzie, costrutti Full e interfaccia
Parte quarta 18. Anatomia del progetto e contratto di conformità 19. L'interprete di riferimento 20. Il seed in codice macchina 21. Verso il self-hosting 22. Gli strumenti di sviluppo
Appendici
- Riferimento dei builtin Core
- Catalogo conformance Core
- Catalogo conformance Full
- Sorgenti degli esempi
- Sorgenti self-hosted
- Documenti di riferimento
- Indice analitico
Manuale di consultazione