Un blocco è un frammento di codice delimitato da parentesi graffe: $\texttt{\{...\}}$. In Scala, un blocco è un’espressione, che può comparire ovunque siano ammesse le espressioni, cioè come corpo di una definizione, ramo di un’espressione condizionale, argomento di una funzione o di un operatore, ecc. (mentre in Java, ad esempio, un blocco è un’istruzione).
Un blocco contiene una sequenza di definizioni ed espressioni; l’ultimo elemento di un blocco è obbligatoriamente un’espressione, che definisce il valore restituito dalla valutazione del blocco.
I blocchi hanno effetti sulla visibilità degli identificatoi:
def
e val
) scritte all’interno di un blocco sono visibili esclusivamente all’interno del bloccoAd esempio, nel codice
val x = 0
def f(y: Int) = y + 1
val result = {
val x = f(3)
x * x
}
l’invocazione di funzione $\texttt{f(3)}$ fa riferimento alla funzione $\texttt f$ definita fuori dal blocco, mentre l’uso del nome $\texttt x$ nell’espressione $\texttt{x * x}$ fa riferimento alla definizione di $\texttt x$ interna al blocco, $\texttt{val x = f(3)}$, che adombra la definizione esterna $\texttt{val x = 0}$.
In generale, il termine lexical scoping (scope lessicale) indica che le regole che determinano la visibilità degli identificatori dipendono solo da come il codice è scrtto, e non da ciò che avviene in fase di esecuzione. Nel linguaggio Scala (come anche Java) si ha appunto lo scope lessicale, mentre ci sono altri linguaggi che usano regole di scope semantiche, legate ai meccanismi di esecuzione del codice.
Tuttavia, specificare che un linguaggio ha regole di scope lessicali non è sufficiente a descrivere completamente tali regole, poichè anche solo nell’ambito dello scope lessicale il progettista di un linguaggio può effettuare diverse scelte di regole (ad esempi, può essere consentito o meno l’adombramento).
Una delle regole di scope di Scala stabilisce che le definizioni esterne a un blocco sono visibili all’interno di un blocco. Ciò è interessante soprattutto nel caso delle funzioni: i parametri formali di una funzione sono visibili nel corpo della funzoine, e quindi anche all’interno dei blocchi che ompaiono nel corpo della funzione, comprese eventuali definizioni di funzioni innestate. Questo significa che, oltre a evitare la name-space pollution, le definizioni innestate fungono anche da meccanismo di semplificazione del codice, perchè si può evitare di passare come argomenti alle funzioni innestate i parametri che sono visibili nel corpo della funzione e che non devono cambiare valore in eventuali chiamate ricorsive.
Ad esempio, si consideri l’implementazione della funzione sqrt
con le funzioni innestate:
def sqrt(z: Double): Double = {
val ERROR = 1e-15
def isGoodEnough(guess: Double, z: Double): Boolean =
abs(guess * guess - z) / z < ERROR
def improveGuess(guess: Double, z: Double): Double =
(guess + z / guess) / 2
def sqrtIter(guess: Double, z: Double): Double =
if (isGoodEnough(guess, z))
guess
else
sqrtIter(improveGuess(guess, z), z)
sqrtIter(z, z)
}
Il parametro formale z
di sqrt
viene passato alle varie funzioni innestate, e il suo valore rimane sempre costante in tutte le chiamate (non viene mai modificato all’interno del corpo della funzione sqrt
). Allora, dato che z
è già visisbile nel corpo di sqrt
, si può evitare di passarlo come parametro alle funzioni innestate:
def sqrt(z: Double): Double = {
val ERROR = 1e-15
def isGoodEnough(guess: Double): Boolean =
abs(guess * guess - z) / z < ERROR
def improveGuess(guess: Double): Double =
(guess + z / guess) / 2
def sqrtIter(guess: Double): Double =
if (isGoodEnough(guess))
guess
else
sqrtIter(improveGuess(guess))
sqrtIter(z)
}
In Scala, a differenza di quanto accade in Java, il punto e virgola (semicolon, ;
) a fine riga è in genere opzionale: è possibile scrivere, ad esempio,