Voi siete qui: Inizio Programmare in Scala

Completare l’indispensabile

Prima di immergerci nel supporto di Scala per la programmazione orientata agli oggetti e funzionale, concludiamo la nostra discussione sulle caratteristiche essenziali che userete nella maggior parte dei vostri programmi.

Operatore? Operatore?

Un concetto fondamentale in Scala è che tutti gli operatori in realtà sono metodi. Considerate il più semplice degli esempi:

// esempi/cap-3/one-plus-two-script.scala

1 + 2

Vedete quel segno più tra i numeri? È un metodo. Primo, Scala vi consente di usare nomi di metodo non alfanumerici: potete chiamare i metodi +, -, $, o in qualunque modo desideriate. Secondo, questa espressione è identica a 1 .+(2). (Abbiamo aggiunto uno spazio dopo 1 perché 1. verrebbe interpretato come un numero Double.) Quando un metodo accetta un argomento, Scala vi permette di omettere sia il punto sia le parentesi, così l’invocazione del metodo sembra l’invocazione di un operatore. Questa notazione viene chiamata “infissa”, perché l’operatore si trova tra l’istanza e l’argomento. Ne sapremo di più tra breve.

Similmente, un metodo senza argomenti può essere invocato senza il punto. Questa notazione viene chiamata “postfissa”.

I programmatori Ruby e Smalltalk ora dovrebbero sentirsi a proprio agio. Come sa chi usa quei linguaggi, queste semplici regole portano grandi benefici quando si vogliono scrivere programmi che scorrono in maniera naturale ed elegante.

Quindi, quali caratteri potete usare negli identificatori? Qui vi presentiamo un breve riepilogo delle regole per gli identificatori usati come nomi dei metodi e dei tipi, come variabili, &c. Per i dettagli precisi, si veda [ScalaSpec2009]. Scala consente di usare tutti i caratteri ASCII stampabili, come lettere, cifre, il trattino basso _ e il simbolo del dollaro $, a eccezione dei caratteri “parentetici” (, ), [, ], {, }, e dei “delimitatori” , , ', ", ., ; e ,. Scala consente di usare anche gli altri caratteri nell’intervallo \u0020\u007F che non appartengono agli insiemi precedenti, come i simboli matematici e “altri” simboli; questi caratteri vengono chiamati caratteri operatore e includono caratteri come /, <, &c.

Le parole riservate non possono essere usate
Come nella maggior parte dei linguaggi, non potete utilizzare le parole riservate come identificatori. Abbiamo elencato le parole riservate nella sezione Parole riservate del capitolo 2. Ricordatevi che alcune di esse sono combinazioni di caratteri di punteggiatura e caratteri operatore. Per esempio, un singolo trattino basso (_) è una parola riservata!
Identificatori semplici – combinazioni di lettere, cifre, $, _ e operatori
Come in Java e in molti linguaggi, un identificatore semplice può cominciare con una lettera o un trattino basso e proseguire con ulteriori lettere, cifre, trattini bassi e simboli del dollaro. Sono permessi anche caratteri Unicode equivalenti. Tuttavia, al pari di Java, Scala riserva il simbolo del dollaro per uso interno, quindi non dovreste usarlo nei vostri identificatori. Dopo un trattino basso, potete avere lettere e cifre oppure una sequenza di caratteri operatore. Il trattino basso è importante, perché dice al compilatore di trattare tutti i caratteri fino allo spazio bianco successivo come parte dell’identificatore. Per esempio, val xyz_++= = 1 assegna il valore 1 alla variabile xyz_++=, mentre l’espressione val xyz++= = 1 non verrà compilata, perché “l’identificatore” potrebbe anche essere interpretato come xyz ++=, che somiglia a un tentativo di aggiungere qualcosa in coda a xyz. Similmente, se avete caratteri operatore dopo il trattino basso, non potete mescolarli con lettere e cifre. Questa restrizione serve a evitare espressioni ambigue come abc_=123: rappresenta l’identificatore abc_=123 oppure l’assegnamento del valore 123 alla variabile abc_?
Identificatori semplici – operatori
Se un identificatore comincia con un carattere operatore, il resto del nome deve essere composto da caratteri operatore.
Letterali tra apici inversi
Un identificatore può anche essere una stringa arbitraria (soggetta alle limitazioni della piattaforma) racchiusa tra due apici inversi, come per esempio val ‵questo è un identificatore valido‵ = "Ciao mondo!". Ricordatevi che questa sintassi si utilizza anche per invocare un metodo su una classe Java o .NET quando il nome del metodo è identico a una parola riservata di Scala, come per esempio java.net.Proxy.‵type‵().
Identificatori per il pattern matching
Nelle espressioni di pattern matching, i lessemi che cominciano con una lettera minuscola vengono riconosciuti come identificatori di variabile, mentre quelli che cominciano con una lettera maiuscola vengono riconosciuti come identificatori di costante. Questa restrizione evita alcune ambigiutà causate dalla sintassi usata per le variabili, che è molto concisa; per esempio, la parola chiave val non è presente.

Zucchero sintattico

Sapendo che tutti gli operatori sono metodi, è più facile ragionare sul codice Scala con cui non avete familiarità. Non dovete preoccuparvi di casi speciali quando vedete nuovi operatori. Lavorando con gli attori nel capitolo 1, avrete notato che abbiamo usato un punto esclamativo (!) per inviare un messaggio a un attore. Ora sapete che ! è semplicemente un altro metodo, come lo sono altri comodi operatori abbreviati che potete usare per comunicare con gli attori. Allo stesso modo, la libreria XML di Scala fornisce gli operatori \ e \\ per navigare la struttura dei documenti; questi sono semplicemente metodi della classe scala.xml.NodeSeq.

Questa denominazione flessibile per i metodi vi mette in grado di scrivere librerie che danno la sensazione di essere una naturale estensione del linguaggio. Potreste scrivere una nuova libreria matematica con tipi numerici che accettano tutti i classici operatori aritmetici come addizione e sottrazione. Potreste scrivere un nuovo livello di messaggistica concorrente che si comporta nello stesso modo degli attori. Le possibilità sono vincolate solo dai limiti che Scala impone alla denominazione dei metodi.

Solo perché potete non significa che dovreste. Quando progettate le vostre librerie e le vostre API in Scala, tenete presente che i programmatori hanno difficoltà a ricordare gli operatori poco intuitivi formati da punteggiatura. Se ne abusate, il vostro codice potrebbe diventare illeggibile a causa del troppo “rumore”. Quando il significato di un’abbreviazione non è immediato, è preferibile aderire alle convenzioni e scrivere per esteso i nomi dei metodi.

Metodi senza parentesi e punti

Per facilitare l’adozione di alcuni stili di programmazione leggibili, Scala è flessibile sull’uso delle parentesi nei metodi. Se un metodo non accetta parametri, potete definirlo senza parentesi; in questo caso, chi lo invoca deve chiamare il metodo senza parentesi. Se aggiungete una coppia vuota di parentesi, allora chi invoca il metodo può opzionalmente aggiungere le parentesi. Per esempio, il metodo size della classe List non ha parentesi, quindi dovete scrivere List(1, 2, 3).size. Se provate a scrivere List(1, 2, 3).size() otterrete un errore. Tuttavia, il metodo length della classe String è definito con le parentesi, quindi le invocazioni "ciao".length() e "ciao".length verranno entrambe compilate.

La convenzione nella comunità Scala prevede di omettere le parentesi quando si invoca un metodo che non ha effetti collaterali. Quindi, la dimensione di una sequenza va richiesta senza parentesi, ma la definizione di un metodo che trasforma gli elementi di una sequenza dovrebbe essere scritta con le parentesi. Questa convenzione segnala i metodi potenzialmente pericolosi agli utenti del vostro codice.

È anche possibile omettere il punto quando si invoca un metodo che non ha parametri o un metodo che accetta solo un argomento. Tenendo presente questa possibilità, il nostro esempio List(1, 2, 3).size potrebbe essere scritto come:

// esempi/cap-3/no-dot-script.scala

List(1, 2, 3) size

Bello, ma poco chiaro. Quand’è che questa flessibilità sintattica si rivela utile? Quando concatenate insieme più invocazioni di metodo in “frasi” di codice espressive e dal significato intuitivo:

// esempi/cap-3/no-dot-better-script.scala

def isEven(n: Int) = (n % 2) == 0

List(1, 2, 3, 4) filter isEven foreach println

Come potreste indovinare, l’esecuzione di questo codice produce l’uscita:

2
4

L’approccio liberale di Scala alle parentesi e ai punti per i metodi fornisce uno degli ingredienti per la creazione di linguaggi domain-specific. Impareremo a conoscere meglio questi linguaggi dopo una breve discussione sulla precedenza degli operatori.

Regole di precedenza

Quindi, se un’espressione come 2.0 * 4.0 / 3.0 * 5.0 è in realtà una serie di chiamate di metodo su Double, quali sono le regole di precedenza degli operatori? Eccole qui, in ordine dalla precedenza più bassa a quella più alta [ScalaSpec2009].

I caratteri sulla stessa riga hanno la stessa precedenza. L’unica eccezione è = che, quando viene usato per l’assegnamento, ha la precedenza più bassa.

Dato che * e / hanno la stessa precedenza, le due espressioni nella sessione scala seguente si comportano allo stesso modo.

scala> 2.0 * 4.0 / 3.0 * 5.0
res2: Double = 13.333333333333332

scala> (((2.0 * 4.0) / 3.0) * 5.0)
res3: Double = 13.333333333333332

In una sequenza di invocazioni associative a sinistra, i metodi legano gli argomenti in ordine da sinistra a destra. “Associative a sinistra”, dite? In Scala, ogni metodo con un nome che termina con i due punti in realtà lega l’argomento alla propria destra, mentre tutti gli altri metodi legano quello alla propria sinistra. Per esempio, potete inserire un elemento in testa a un’istanza di List usando il metodo :: (chiamato “cons”, abbreviazione di “constructor”, cioè “costruttore”).

scala> val list = List('b', 'c', 'd')
list: List[Char] = List(b, c, d)

scala> 'a' :: list
res4: List[Char] = List(a, b, c, d)

La seconda espressione è equivalente a list.::('a'). In una sequenza di invocazioni associative a destra, i metodi legano gli argomenti da destra a sinistra. E cosa succede nelle espressioni che mescolano le associatività?

scala> 'a' :: list ++ List('e', 'f')
res5: List[Char] = List(a, b, c, d, e, f)

(Il metodo ++ concatena due liste.) In questo caso, list viene aggiunta a List('e', 'f'), poi 'a' viene inserita in testa per creare la lista finale. Di solito è meglio aggiungere le parentesi per evitare qualsiasi possibile incertezza.

Tutti i metodi il cui nome finisce con : legano l’argomento alla propria destra, non alla propria sinistra.

Infine notate che, quando usate il comando scala, interattivamente o con gli script, potrebbe sembrare che stiate definendo variabili “globali” e metodi al di fuori dei tipi. In realtà, questa è un’illusione: l’interprete racchiude tutte le definizioni in un tipo anonimo prima di generare il bytecode per la JVM o per il CLR.

Linguaggi domain-specific

I linguaggi domain-specific, o DSL, forniscono un mezzo sintattico conveniente per esprimere obiettivi nel dominio di un dato problema. Per esempio, SQL possiede le funzionalità di un linguaggio di programmazione sufficienti soltanto a lavorare con un database, qualificandosi dunque come un linguaggio specifico per quel dominio.

Mentre alcuni DSL come SQL sono autocontenuti, si è diffusa la pratica di implementare i DSL come sottoinsiemi di linguaggi di programmazione completi. In questo modo, i programmatori possono sfruttare l’intero linguaggio ospite per i casi limite che il DSL non copre e risparmiarsi la fatica di scrivere analizzatori lessicali, analizzatori sintattici, e tutti gli altri componenti di un interprete.

La sintassi ricca e flessibile di Scala rende la scrittura dei DSL un gioco da ragazzi. Considerate questo esempio di uno stile di scrittura dei test chiamato sviluppo guidato dal comportamento (in inglese, behavior-driven development) [BDD] che usa la libreria Specs (si veda la sezione Specs nel capitolo 14).

// esempi/cap-3/specs-script.scala
// Frammento di esempio di uno script Specs. Non funziona da solo.

"il cercatore di nerd" should {
  "identificare i nerd in una lista" in {
    val actors = List("Rick Moranis", "James Dean", "Woody Allen")
    val finder = new NerdFinder(actors)
    finder.findNerds mustEqual List("Rick Moranis", "Woody Allen")
  }
}

Notate quanto sia facile leggere queste righe di codice come frasi in linguaggio naturale:1 “questo dovrebbe (should) collaudare quello nello (in) scenario seguente”, “questo valore deve essere uguale (mustEqual) a quel valore”, e così via. Questo esempio usa la superba libreria Specs, che fornisce un DSL efficace per sfruttare il processo di sviluppo guidato dal comportamento come metodologia ingegneristica e di collaudo. Facendo massimo uso della sintassi liberale e dei potenti metodi di Scala, i test scritti con Specs sono comprensibili anche da chi non è uno sviluppatore.

Questo è solo un assaggio della potenza dei DSL in Scala. Ne vedremo altri esempi più avanti e impareremo come scrivere i nostri man mano che faremo progressi (si veda il capitolo 11).

Le istruzioni if in Scala

Anche gli aspetti linguistici più familiari sono potenziati in Scala; ci basta dare un’occhiata alla modesta istruzione if. Come in quasi tutti i linguaggi, l’istruzione if in Scala valuta un’espressione condizionale, poi procede verso un blocco se il risultato è true oppure diverge verso un blocco alternativo se il risultato è false. Ecco un semplice esempio:

// esempi/cap-3/if-script.scala

if (2 + 2 == 5) {
  println("Saluti dal 1984.")
} else if (2 + 2 == 3) {
    println("Saluti da un corso di recupero in matematica?")
} else {
  println("Saluti da un futuro non orwelliano.")
}

La differenza è che in Scala if e quasi tutte le altre istruzioni in realtà sono espressioni. Quindi, possiamo assegnare il risultato di un’espressione if, come mostrato in questo esempio:

// esempi/cap-3/assigned-if-script.scala

val configFile = new java.io.File(".myapprc")

val configFilePath = if (configFile.exists()) {
  configFile.getAbsolutePath()
} else {
  configFile.createNewFile()
  configFile.getAbsolutePath()
}

Notate che le istruzioni if sono espressioni, nel senso che hanno un valore. In questo esempio, la variabile configFilePath contiene il risultato di un’espressione if che gestisce internamente il caso in cui un file di configurazione non esiste, poi restituisce il percorso assoluto di quel file. Questo valore ora può essere riutilizzato nell’intera applicazione senza che l’espressione if venga valutata nuovamente ogni volta.

Dato che in Scala le istruzioni if sono espressioni, non c’è alcun bisogno del caso particolare rappresentato dalle espressioni condizionali ternarie presenti nei linguaggi derivati dal C. Non vedrete x ? faiQuesto() : faiQuello() in Scala, perché Scala fornisce un meccanismo che è altrettanto potente e più leggibile.

Cosa succede se omettiamo la clausola else nell’esempio precedente? Otterremo la risposta digitando il codice nell’interprete scala.

scala> val configFile = new java.io.File("~/.myapprc")
configFile: java.io.File = ~/.myapprc

scala> val configFilePath = if (configFile.exists()) {
     |   configFile.getAbsolutePath()
     | }
configFilePath: Unit = ()

scala>

Notate che ora configFilePath è un’istanza di Unit. (Prima era un’istanza di String.) L’inferenza di tipo sceglie un tipo valido per tutti i risultati dell’espressione if; in questo caso Unit è l’unica alternativa, dato che “nessun valore” è uno dei possibili risultati.

Le espressioni for in Scala

Un’altra struttura di controllo ordinaria particolarmente potente in Scala è il ciclo for, che la comunità Scala chiama espressione for o descrizione for. Questa parte del linguaggio merita quantomeno un nome stravagante, perché è in grado di fare alcuni giochi di prestigio fantastici.

In effetti, il termine “descrizione” viene dalla programmazione funzionale. Esprime l’idea di attraversare un qualche tipo di insieme, “descrivendo” ciò che vogliamo trovare allo scopo di usarlo per calcolare qualcosa di nuovo.

Un semplice esempio

Cominciamo con una espressione for di base:

// esempi/cap-3/basic-for-script.scala

val dogBreeds = List("Dobermann", "Yorkshire Terrier", "Bassotto",
                     "Scottish Terrier", "Alano", "Cane d'acqua portoghese")

for (breed <- dogBreeds)
  println(breed)

Come potreste indovinare, questo codice dice: “per ogni elemento nella lista dogBreeds, crea una variabile temporanea chiamata breed con il valore di quell’elemento e poi stampala”. Pensate all’operatore <- come a una freccia che assegna gli elementi di una collezione, uno per uno, alla variabile attraverso la quale vi faremo riferimento all’interno della espressione for. L’operatore freccia-a-sinistra viene chiamato generatore perché genera i singoli valori di una collezione per usarli in una espressione.

Filtrare gli elementi

E se volessimo una grana più fine? Le espressioni for in Scala ci consentono di usare filtri per specificare gli elementi di una collezione con i quali vogliamo lavorare. Quindi, per trovare tutti i Terrier nella nostra lista di razze canine, potremmo modificare l’esempio precedente in questo modo:

// esempi/cap-3/filtered-for-script.scala

val dogBreeds = List("Dobermann", "Yorkshire Terrier", "Bassotto",
                     "Scottish Terrier", "Alano", "Cane d'acqua portoghese")
for (breed <- dogBreeds
  if breed.contains("Terrier")
) println(breed)

Per aggiungere più di un filtro a una espressione for, separate i filtri con un punto e virgola.

// esempi/cap-3/double-filtered-for-script.scala

val dogBreeds = List("Dobermann", "Yorkshire Terrier", "Bassotto",
                     "Scottish Terrier", "Alano", "Cane d'acqua portoghese")
for (breed <- dogBreeds
  if breed.contains("Terrier");
  if !breed.startsWith("Yorkshire")
) println(breed)

Ora avete trovato tutti i Terrier che non vengono dallo Yorkshire, e ci auguriamo che abbiate imparato quanto possono essere utili i filtri nella elaborazione.

Produrre gli elementi in uscita

E se invece di stampare la vostra collezione filtrata aveste bisogno di passarla a un’altra parte del vostro programma? La parola chiave yield è la soluzione che vi permette di generare nuove collezioni con le espressioni for. Notate che, nell’esempio seguente, abbiamo racchiuso l’espressione for tra parentesi graffe, come faremmo per definire un blocco qualsiasi.

Le espressioni for possono essere definite con parentesi tonde o graffe, ma se usate le parentesti graffe non siete obbligati a separare i vostri filtri con il punto e virgola. Molto spesso preferirete usare le parentesi graffe quando avete più di un filtro, più di un assegnamento, &c.

// esempi/cap-3/yielding-for-script.scala

val dogBreeds = List("Dobermann", "Yorkshire Terrier", "Bassotto",
                     "Scottish Terrier", "Alano", "Cane d'acqua portoghese")
val filteredBreeds = for {
  breed <- dogBreeds
  if breed.contains("Terrier")
  if !breed.startsWith("Yorkshire")
} yield breed

Ogni volta che l’espressione for viene attraversata, il risultato filtrato viene prodotto in uscita sotto forma di un valore chiamato breed. Questi risultati si accumulano a ogni passaggio, e la collezione risultante viene assegnata alla variabile filteredBreeds (come abbiamo fatto più indietro con l’istruzione if). Il tipo della collezione risultante da una espressione for/yield viene inferito a partire dal tipo della collezione su cui si sta iterando. In questo caso filteredBreeds è di tipo List[String], essendo un sottoinsieme della lista dogBreeds che è anch’essa di tipo List[String].

Ambito esteso

L’ultima caratteristica utile delle espressioni for di Scala è la possibilità di definire variabili all’interno della prima parte della vostra espressione for per poi usarle nell’ultima parte. Questo si capisce meglio con un esempio:

// esempi/cap-3/scoped-for-script.scala

val dogBreeds = List("Dobermann", "Yorkshire Terrier", "Bassotto",
                     "Scottish Terrier", "Alano", "Cane d'acqua portoghese")
for {
  breed <- dogBreeds
  upcasedBreed = breed.toUpperCase()
} println(upcasedBreed)

Notate che, anche senza dichiarare upcasedBreed come val, potete riutilizzarla all’interno del corpo della vostra espressione for. Questo approccio è ideale per trasformare gli elementi di una collezione man mano che la attraversate.

Infine, nella sezione Option e le espressioni for del capitolo 13 vedremo come l’uso di Option con le espressioni for possa ridurre notevolmente la dimensione del codice eliminando controlli superflui per valori “nulli” o “mancanti”.

Altri costrutti di ciclo

Scala possiede alcuni ulteriori costrutti di ciclo.

I cicli while in Scala

Presente in molti linguaggi, il ciclo while esegue un blocco di codice fino a quando una condizione si mantiene vera. Per esempio, il codice seguente stampa un reclamo una volta al giorno fino al prossimo venerdì 13.

// esempi/cap-3/while-script.scala
// ATTENZIONE: l'esecuzione di questo script dura MOOOOLTO tempo!

import java.util.Calendar

def isFridayThirteen(cal: Calendar): Boolean = {
  val dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)
  val dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)

  // Scala restituisce il risultato dell'ultima espressione in un metodo
  (dayOfWeek == Calendar.FRIDAY) && (dayOfMonth == 13)
}

while (!isFridayThirteen(Calendar.getInstance())) {
  println("Oggi non è venerdì 13. Che indecenza.")
  // riposa per un giorno
  Thread.sleep(86400000)
}

Più avanti potete trovare una tabella degli operatori condizionali che si possono usare con i cicli while.

I cicli do/while in Scala

Come il ciclo while appena visto, un ciclo do/while esegue un blocco di codice fino a quando un’espressione condizionale è vera. L’unica differenza è che un ciclo do/while controlla se la condizione è vera dopo aver eseguito il blocco. Per contare fino a dieci, potremmo scrivere questo:

// esempi/cap-3/do-while-script.scala

var count = 0

do {
  count += 1
  println(count)
} while (count < 10)

A quanto pare, c’è un modo più elegante per attraversare le collezioni in Scala, come vedremo nella prossima sezione.

Espressioni generatore

Ricordate l’operatore freccia (<-) dalla discussione precedente sui cicli for? Possiamo metterlo all’opera anche qui. Ritocchiamo l’ultimo esempio sul ciclo do/while:

// esempi/cap-3/generator-script.scala

for (i <- 1 to 10) println(i)

Sì, non c’è bisogno d’altro. Questa singola riga così pulita è possibile grazie alla classe RichInt di Scala. Il compilatore invoca una conversione implicita per convertire 1, che è un Int, in un RichInt. (Parleremo di queste conversioni nella sezione La gerarchia di tipi di Scala del capitolo 7 e nella sezione Conversioni implicite del capitolo 8.) La classe RichInt definisce un metodo to che prende un altro intero e restituisce un’istanza di Range.Inclusive. Inclusive è una classe annidata nell’oggetto associato Range (un concetto che abbiamo introdotto brevemente nel capitolo 1; si veda il capitolo 6 per i dettagli). Questa sottoclasse della classe Range eredita un certo numero di metodi per lavorare con le sequenze e con le strutture dati iterabili, compresi quelli necessari per essere usata in un ciclo for.

A proposito, se volete contare da 1 fino a 10 escluso, potete usare until invece di to, come in for (i <- 1 until 10).

Questo dovrebbe fornirvi un quadro più chiaro di come le librerie interne di Scala si compongono per formare costrutti linguistici facili da usare.

Quando lavorate con i cicli nella maggior parte dei linguaggi, potete uscire da un ciclo usando break o passare all’iterazione successiva usando continue. Scala non è dotato di queste istruzioni, perché non sono necessarie se scrivete codice Scala idiomatico. Usate le espressioni condizionali per controllare se un ciclo debba continuare, oppure fate uso della ricorsione. Ancora meglio, filtrate le vostre collezioni in anticipo per eliminare condizioni complesse all’interno dei vostri cicli. Tuttavia, a causa delle molte richieste, la versione 2.8 di Scala include il supporto per break, sotto forma di metodo di libreria piuttosto che di parola chiave predefinita.

Operatori condizionali

Scala prende in prestito molti operatori condizionali da Java e dai suoi predecessori. Troverete gli operatori che seguono nelle istruzioni if, nei cicli while e dovunque si possa applicare una condizione.

Tabella 3.1. Operatori condizionali.

OperatoreOperazioneDescrizione

&&

congiunzione

I valori alla destra e alla sinistra dell’operatore sono veri. La parte destra viene valutata solo se la parte sinistra è vera.

||

disgiunzione

Almeno uno dei valori alla sinistra o alla destra dell’operatore è vero. La parte destra viene valutata solo se la parte sinistra è falsa.

>

maggiore di

Il valore sulla sinistra è maggiore del valore sulla destra.

>=

maggiore o uguale a

Il valore sulla sinistra è maggiore o uguale al valore sulla destra.

<

minore di

Il valore sulla sinistra è minore del valore sulla destra.

<=

minore o uguale a

Il valore sulla sinistra è minore o uguale al valore sulla destra.

==

uguaglianza

Il valore sulla sinistra è uguale al valore sulla destra.

!=

disugualianza

Il valore sulla sinistra è diverso dal valore sulla destra.

Notate che && e || sono operatori “a corto circuito”. Interrompono la valutazione delle espressioni appena ne conoscono il risultato.

Discuteremo l’uguaglianza tra gli oggetti in maggior dettaglio nella sezione L’uguaglianza tra oggetti del capitolo 6. Per esempio, vedremo che == ha un significato differente in Scala rispetto a Java. A parte questo, gli altri operatori dovrebbero esservi già noti, quindi proseguiamo con qualcosa di nuovo ed eccitante.

Pattern matching

Un’idea presa in prestito dai linguaggi funzionali, il pattern matching è un modo potente e conciso di compiere scelte programmatiche tra molteplici condizioni. Il pattern matching equivale alla consueta istruzione case dei linguaggi derivati dal C, ma potenziata. Nella tipica istruzione case siete vincolati a cercare corrispondenze con valori di tipi ordinali, producendo espressioni banali come “nel caso in cui i sia 5, stampa un messaggio; nel caso in cui i sia 6, esci dal programma”. Con il pattern matching di Scala i vostri casi possono includere tipi, wildcard, sequenze, e persino ispezionare in profondità le variabili di un oggetto.

Una semplice corrispondenza

Per cominciare, simuliamo il lancio di una moneta cercando una corrispondenza con il valore di un booleano:

// esempi/cap-3/match-boolean-script.scala

val bools = List(true, false)

for (bool <- bools) {
  bool match {
    case true => println("testa")
    case false => println("croce")
    case _ => println("qualcosa di diverso da testa o croce (aaagh!)")
  }
}

Sembra proprio un’istruzione case nello stile del C, giusto? L’unica differenza è l’ultima clausola con il trattino basso _ come wildcard che corrisponde a qualsiasi cosa non definita nei casi precedenti e che quindi serve allo stesso scopo della parola chiave default nelle istruzioni switch di Java e C#.

Il pattern matching è avido: la prima corrispondenza vince. Quindi, se provate a mettere una clausola case _ prima di tutte le altre clausole case, il compilatore segnalerà un errore di “codice irraggiungibile” sulla clausola successiva, perché niente riuscirà a oltrepassare la clausola di default!

Usate case _ per la corrispondenza jolly “acchiappatutto” di default.

E se volessimo lavorare con le corrispondenze come variabili?

Usare variabili nelle corrispondenze

// esempi/cap-3/match-variable-script.scala

import scala.util.Random

val randomInt = new Random().nextInt(10)

randomInt match {
  case 7 => println("sette fortunato!")
  case otherNumber => println("buu, il vecchio e noioso " + otherNumber)
}

In questo esempio, assegniamo il valore catturato dalla clausola case jolly a una variabile chiamata otherNumber, poi stampiamo la variabile nell’espressione successiva. Se generiamo un sette, decanteremo le virtù di questo numero; altrimenti, malediremo la sorte subita che ci ha fatto pescare un numero sfortunato.

Corrispondenze sul tipo

Questi semplici esempi non cominciano nemmeno a scalfire la superficie delle caratteristiche del pattern matching di Scala. Proviamo una corrispondenza sulla base del tipo:

// esempi/cap-3/match-type-script.scala

val sundries = List(23, "Ciao", 8.5, 'q')

for (sundry <- sundries) {
  sundry match {
    case i: Int => println("ecco un Int: " + i)
    case s: String => println("ecco una String: " + s)
    case f: Double => println("ecco un Double: " + f)
    case other => println("ecco qualcos'altro: " + other)
  }
}

Qui estraiamo ogni elemento da una lista di elementi di qualsiasi tipo che in questo caso contiene istanze di String, Double, Int e Char. Per i primi tre tipi, facciamo sapere all’utente quale tipo particolare abbiamo estratto e qual è il suo valore; quando otteniamo qualcosa di diverso (l’istanza di tipo Char), facciamo sapere all’utente solo il valore. Potremmo aggiungere alla lista altri elementi di tipo diverso e questi verrebbero catturati dalla clausola jolly finale nella variabile other.

Corrispondenze sulle sequenze

Dato che lavorare con Scala significa spesso lavorare con le sequenze, non sarebbe comodo poter cercare una corrispondenza sulla base della lunghezza e del contenuto di liste e array? L’esempio seguente fa proprio questo, controllando se due liste contengono quattro elementi, il secondo dei quali deve essere il numero intero 3.

// esempi/cap-3/match-seq-script.scala

val willWork = List(1, 3, 23, 90)
val willNotWork = List(4, 18, 52)
val empty = List()

for (l <- List(willWork, willNotWork, empty)) {
  l match {
    case List(_, 3, _, _) => println("Quattro elementi, di cui il 2° è '3'.")
    case List(_*) => println("Qualsiasi altra lista con 0 o più elementi.")
  }
}

Nella seconda clausola case abbiamo usato una wildcard speciale per trovare una corrispondenza con istanze di List di qualunque dimensione, anche zero, che contengono elementi di qualunque tipo. Potete usare questo pattern al termine di ogni corrispondenza sulle sequenze per rimuovere la condizione sulla lunghezza.

Ricordate il metodo “cons” delle liste, ::, che abbiamo menzionato in precedenza? L’espressione a :: lista inserisce a in testa a una lista. Potete usare questo operatore anche per estrarre la testa e la coda di una lista.

// esempi/cap-3/match-list-script.scala

val willWork = List(1, 3, 23, 90)
val willNotWork = List(4, 18, 52)
val empty = List()

def processList(l: List[Any]): Unit = l match {
  case head :: tail =>
    format("%s ", head)
    processList(tail)
  case Nil => println("")
}

for (l <- List(willWork, willNotWork, empty)) {
  print("Lista: ")
  processList(l)
}

Il metodo processList cerca una corrispondenza a partire dall’argomento l di tipo List. L’inizio della definizione di metodo potrebbe sembrarvi strano.

def processList(l: List[Any]): Unit = l match {
  …
}

Ci auguriamo che nascondere i dettagli con i punti di sospensione renda il significato un po’ più chiaro. Il metodo processList in realtà è composto da una sola istruzione che si estende su più righe.

Come prima cosa, l’istruzione cerca una corrispondenza con head :: tail, assegnando il primo elemento della lista a head e il resto della lista a tail; stiamo estraendo la testa e la coda della lista usando ::. Quando questa clausola case trova una corrispondenza, stampa head e invoca ricorsivamente processList per lavorare sulla coda.

La seconda clausola case corrisponde alla lista vuota, Nil. Stampa un carattere di fine riga e termina la ricorsione.

Corrispondenze su tuple (e guardie)

In alternativa, se volessimo controllare di avere una tupla di due elementi, potremmo cercare una corrispondenza con le tuple:

// esempi/cap-3/match-tuple-script.scala

val tupA = ("Buona", "giornata!")
val tupB = ("Guten", "Tag!")

for (tup <- List(tupA, tupB)) {
  tup match {
    case (thingOne, thingTwo) if thingOne == "Buona" =>
        println("Una 2-tupla che comincia con 'Buona'.")
    case (thingOne, thingTwo) =>
        println("Questa contiene due cose: " + thingOne + " e " + thingTwo)
  }
}

Nella seconda clausola case di questo esempio abbiamo estratto i valori contenuti nella tupla assegnandoli ad alcune variabili, poi abbiamo riutilizzato quelle variabili nell’espressione successiva.

Nella prima clausola abbiamo aggiunto un nuovo concetto: le guardie. La condizione if dopo la tupla è una guardia. La guardia viene valutata durante la ricerca di una corrispondenza, nel momento in cui le variabili nella parte precedente della clausola vengono estratte. Le guardie offrono una granularità aggiuntiva quando si costruiscono i casi. In questo esempio, l’unica differenza tra i due pattern è l’espressione di guardia, ma questo basta al compilatore per distinguerli.

Ricordatevi che le clausole del pattern matching vengono valutate in ordine. Per esempio, se il vostro primo caso è più generale del secondo, il secondo caso non verrà mai raggiunto. (I casi non raggiungibili causeranno un errore di compilazione.) Potete includere un caso “di default” alla fine di un costrutto di pattern matching, usando come wildcard un trattino basso o una variabile con un nome significativo. Quando usate una variabile, essa non dovrebbe avere alcun tipo esplicito oppure dovrebbe essere dichiarata come Any, in modo da poter corrispondere a qualsiasi cosa. Tuttavia, cercate di progettare il vostro codice per evitare di usare una clausola jolly, assicurandovi che il costrutto riceva solo gli specifici elementi attesi.

Corrispondenze sulle classi case

Proviamo a cercare una corrispondenza in profondità, esaminando il contenuto degli oggetti nel pattern matching.

// esempi/cap-3/match-deep-script.scala

case class Person(name: String, age: Int)

val alice = new Person("Alice", 25)
val bob = new Person("Bob", 32)
val charlie = new Person("Charlie", 32)

for (person <- List(alice, bob, charlie)) {
  person match {
    case Person("Alice", 25) => println("Ciao Alice!")
    case Person("Bob", 32) => println("Ciao Bob!")
    case Person(name, age) =>
      println("Chi sei tu, persona di " + age + " anni chiamata " + name + "?")
  }
}

Il povero Charlie viene trattato con freddezza, come possiamo vedere nell’uscita di questo esempio:

Ciao Alice!
Ciao Bob!
Chi sei tu, persona di 32 anni chiamata Charlie?

Come prima cosa, definiamo una classe case, un tipo particolare di classe che impareremo a conoscere nella sezione Classi case del capitolo 6. Per ora, sarà sufficiente dire che una classe case consente di costruire in maniera molto sintetica oggetti semplici dotati di alcuni metodi predefiniti. Il pattern matching cerca Alice e Bob ispezionando i valori passati al costruttore di Person, la nostra classe case. Charlie ricade nella clausola acchiappatutto pur avendo lo stesso valore di age che ha Bob, poiché la corrispondenza viene cercata anche sulla proprietà name.

Questo tipo di corrispondenza diventa estremamente utile quando si lavora con gli attori, come vedremo più avanti. Le istanze di classi case vengono frequentemente inviate come messaggi agli attori, e il pattern matching in profondità sui contenuti di un oggetto è un modo conveniente per “esaminare” quei messaggi.

Corrispondenze su espressioni regolari

Le espressioni regolari sono convenienti per estrarre dati da stringhe che hanno una struttura informale ma che non sono “dati strutturati” (cioè, non sono in formato XML o JSON, per esempio). Chiamate comunemente regex, le espressioni regolari sono una caratteristica presente in quasi tutti i linguaggi di programmazione moderni. Esse forniscono una sintassi concisa per specificare corrispondenze complesse, e dietro le quinte vengono tipicamente trasformate in macchine a stati per ottimizzare le prestazioni.

Le espressioni regolari in Scala non dovrebbero contenere sorprese se le avete usate in altri linguaggi di programmazione. Vediamone un esempio.

// esempi/cap-3/match-regex-script.scala

val BookExtractorRE = """Libro: titolo=([^,]+),\s+autori=(.+)""".r
val MagazineExtractorRE = """Rivista: titolo=([^,]+),\s+numero=(.+)""".r

val catalog = List(
  "Libro: titolo=Programmare in Scala, autori=Dean Wampler, Alex Payne",
  "Rivista: titolo=The New Yorker, numero=Gennaio 2009",
  "Libro: titolo=Guerra e pace, autori=Lev Tolstoj",
  "Rivista: titolo=The Atlantic, numero=Febbraio 2009",
  "DatiInvalidi: testo=Chi ha messo qui questa roba?"
)

for (item <- catalog) {
  item match {
    case BookExtractorRE(title, authors) =>
      println("Libro \"" + titolo + "\", scritto da " + authors)
    case MagazineExtractorRE(title, issue) =>
      println("Rivista \"" + titolo + "\", numero " + issue)
    case entry => println("Registrazione non riconosciuta: " + entry)
  }
}

Cominciamo con due espressioni regolari, una per le registrazioni di libri e un’altra per le registrazioni di riviste. L’invocazione di r su una stringa la trasforma in un’espressione regolare; qui usiamo stringhe con triple virgolette per evitare di dover effettuare l’escape delle barre inverse. Se doveste trovare poco chiaro il metodo r di trasformazione delle stringhe, potete anche definire le espressioni regolari creando nuove istanze della classe Regex, come in new Regex("""\W""").

Notate che ognuna delle nostre espressioni regolari definisce due gruppi di cattura denotati dalle parentesi. Ogni gruppo cattura il valore di un singolo campo nella registrazione, come l’autore o il titolo di un libro. In Scala, le espressioni regolari convertono questi gruppi di cattura in estrattori. Ogni corrispondenza trovata imposta un campo al risultato catturato; ogni corrispondenza mancata imposta il campo a null.

Cosa significa questo in pratica? Se il testo dato in pasto all’espressione regolare corrisponde, case BookExtractorRE(title, authors) assegnerà il primo gruppo di cattura a title e il secondo a authors. Possiamo quindi usare quei valori sulla parte destra della clausola case, come abbiamo fatto nel nostro esempio. I nomi delle variabili title e author nell’estrattore sono arbitrari; le corrispondenze dei gruppi di cattura vengono semplicemente assegnate da sinistra a destra, e potete dare loro i nomi che preferite.

Queste sono le espressioni regolari di Scala in breve. La classe scala.util.matching.Regex offre diversi metodi comodi per trovare e sostituire le corrispondenze in una stringa, sia per tutte le occorrenze sia soltanto per la prima occorrenza, quindi fatene buon uso.

In questa sezione non parleremo di come si scrivono le espressioni regolari. La classe Regex di Scala usa le API della libreria di espressioni regolari della piattaforma sottostante (cioè, la libreria di Java o di .NET). Consultate le guide di riferimento per quelle API se ne volete conoscere i dettagli più complicati, in quanto potrebbero esserci sottili differenze rispetto al supporto per le espressioni regolari nel vostro linguaggio preferito.

Legare variabili annidate nelle clausole case

A volte volete legare una variabile a un oggetto racchiuso in una corrispondenza nel caso in cui state specificando criteri di corrispondenza anche per l’oggetto annidato. Supponete di modificare l’esempio di Person appena visto in modo da cercare una corrispondenza sulle coppie chiave-valore di una mappa. Memorizzeremo gli stessi oggetti Person come valori e useremo un identificatore numerico come chiave. Aggiungeremo anche un altro attributo a Person, un campo role che punta a un oggetto proveniente da una gerarchia di tipi.

// esempi/cap-3/match-deep-pair-script.scala

class Role
case object Manager extends Role
case object Developer extends Role

case class Person(name: String, age: Int, role: Role)

val alice = new Person("Alice", 25, Developer)
val bob = new Person("Bob", 32, Manager)
val charlie = new Person("Charlie", 32, Developer)

for (item <- Map(1 -> alice, 2 -> bob, 3 -> charlie)) {
  item match {
    case (id, p @ Person(_, _, Manager)) => format("%s è sottopagata.\n", p)
    case (id, p @ Person(_, _, _)) => format("%s è strapagata.\n", p)
  }
}

I case object non sono altro che oggetti singleton come quelli che abbiamo visto in precedenza, ma con lo speciale comportamento delle classi case. Siamo soprattutto interessati all’espressione p @ Person(…) incorporata nella clausola case. Stiamo cercando una corrispondenza con una categoria particolare di oggetti Person all’interno della tupla. Vogliamo anche assegnare l’istanza di Person a una variabile p in modo da poterla stampare.

Person(Alice,25,Developer) è sottopagata.
Person(Bob,32,Manager) è strapagata.
Person(Charlie,32,Developer) è sottopagata.

Se non usassimo criteri per la corrispondenza con Person, potremmo semplicemente scrivere p: Person. Per esempio, la clausola match precedente potrebbe essere scritta in questo modo.

item match {
  case (id, p: Person) => p.role match {
    case Manager => format("%s è sottopagata.\n", p)
    case _ => format("%s è strapagata.\n", p)
  }
}

Notate che la sintassi p @ Person(…) ci offre un modo per appiattire le istruzioni match annidate ottenendo una sola istruzione. È simile all’uso che si fa dei “gruppi di cattura“ in un’espressione regolare per estrarre le sottostringhe che vogliamo invece di suddividere la stringa in diversi passi successivi. Adoperate la tecnica che preferite.

Usare le clausole try, catch e finally

Sfruttando i costrutti funzionali e la tipizzazione forte, Scala incoraggia uno stile di codifica che riduce il bisogno di usare e gestire le eccezioni. Ma quando Scala interagisce con Java, le eccezioni prevalgono ancora.

Scala non possiede eccezioni controllate, a differenza di Java. Persino le eccezioni controllate di Java sono trattate come non controllate da Scala. Non c’è alcuna clausola throws sulle dichiarazioni di metodo. Tuttavia, esiste un’annotazione @throws utile per interoperare con Java. Si veda la sezione Annotazioni nel capitolo 13.

Per fortuna, Scala tratta la gestione delle eccezioni semplicemente come un altro caso di pattern matching, permettendoci di fare scelte intelligenti quando affrontiamo una molteplicità di potenziali eccezioni. Vediamola in azione.

// esempi/cap-3/try-catch-script.scala

import java.util.Calendar

val then = null
val now = Calendar.getInstance()

try {
  now.compareTo(then)
} catch {
  case e: NullPointerException => println("Uno era null!"); System.exit(-1)
  case unknown => println("Eccezione sconosciuta " + unknown); System.exit(-1)
} finally {
  println("Ha funzionato tutto.")
  System.exit(0)
}

In questo esempio, abbiamo esplicitamente catturato l’eccezione NullPointerException che viene lanciata quando proviamo a confrontare un’istanza di Calendar con null. Abbiamo anche definito unknown come clausola jolly, giusto per sicurezza. Se non stessimo scrivendo questo programma per farlo fallire, il blocco finally verrebbe raggiunto e l’utente verrebbe informato che ogni cosa ha funzionato alla perfezione.

Potete usare un trattino basso (la wildcard standard di Scala) come segnaposto per catturare ogni tipo di eccezione (in realtà, per trovare una corrispondenza con qualsiasi oggetto in un’espressione di pattern matching). Tuttavia, non sarete in grado di fare riferimento all’eccezione nelle espressioni successive. Date un nome alla variabile che contiene l’eccezione se ne avete bisogno, per esempio se volete stampare l’eccezione come abbiamo fatto nella clausola jolly dell’esempio precedente; e o ex sono nomi accettabili.

A parte il pattern matching, il trattamento che Scala riserva alla gestione delle eccezioni dovrebbe essere familiare a coloro che programmano correntemente in Java, Ruby, Python e nella maggior parte dei linguaggi mainstream. E sì, le eccezioni si lanciano scrivendo throw new MyBadException(…). Non c’è altro da dire.

Considerazioni conclusive sul pattern matching

Quando viene usato in maniera appropriata, il pattern matching è un modo potente ed elegante per estrarre informazioni dagli oggetti. Se ricordate, nel capitolo 1 abbiamo sottolineato la sinergia tra pattern matching e polimorfismo. La maggior parte delle volte volete evitare i problemi delle istruzioni switch che lavorano con una gerarchia di classi, perché devono essere modificate ogni volta che la gerarchia viene cambiata.

Nel nostro esempio degli attori che disegnano abbiamo usato il pattern matching per separare le differenti “categorie” di messaggi, ma abbiamo sfruttato il polimorfismo per disegnare le forme che venivano inviate. Potremmo cambiare la gerarchia di Shape e il codice che usa gli attori non richiederebbe alcuna modifica.

Il pattern matching è utile anche per quei problemi di progettazione in cui avete bisogno di ottenere dati che si trovano dentro un oggetto, ma solo in circostanze particolari. Una delle conseguenze non intenzionali della specifica JavaBeans [JavaBeansSpec] è quella di aver incoraggiato i programmatori a esporre i campi dei propri oggetti attraverso metodi per ottenere e impostare i valori. Questa non dovrebbe mai essere la decisione predefinita. L’accesso alla “informazione di stato” dovrebbe essere incapsulato e venire esposto solo in modi che hanno senso per il tipo dal punto di vista dell’astrazione che rappresenta.

Invece, considerate l’uso del pattern matching per quei “rari” casi in cui avete bisogno di estrarre informazioni in maniera controllata. Come vedremo nella sezione Il metodo unapply del capitolo 6, gli esempi di pattern matching che vi abbiamo mostrato usano i metodi unapply definiti per estrarre informazioni dalle istanze. Questi metodi vi permettono di estrarre le informazioni desiderate tenendo nascosti i dettagli di implementazione. In effetti, le informazioni restituite da un metodo unapply potrebbero essere una trasformazione delle reale informazioni contenute nell’istanza.

Infine, quando progettate istruzioni di pattern matching, guardatevi dal fare affidamento su una clausola case di default. In quali circostanze “nessuna delle precedenti” sarebbe la risposta corretta? La presenza di una tale clausola potrebbe indicare che il progetto deve essere ritoccato in modo che sappiate più precisamente quali corrispondenze potrebbero verificarsi. Impareremo una tecnica che vi sarà di aiuto quando discuteremo le gerarchie di classi sigillate nella sezione Gerarchie di classi sigillate del capitolo 7.

Enumerazioni

Ricordate i nostri esempi che coinvolgevano varie razze canine? Ragionando sui tipi in quei programmi, potremmo voler introdurre un tipo radice Breed che tenga traccia del numero di razze. Un tipo come questo viene chiamato tipo enumerato e i valori che contiene vengono chiamati enumerazioni.

Sebbene le enumerazioni siano una parte predefinita di molti linguaggi di programmazione, Scala prende una strada diversa e le implementa come classi nella sua libreria standard. Questo significa che non c’è alcuna sintassi particolare per le enumerazioni in Scala, a differenza di quanto accade in Java e C#; invece, vi basta definire un oggetto che estende la classe Enumeration. Quindi, a livello di bytecode, non c’è nessun collegamento tra le enumerazioni di Scala e i costrutti enum di Java e C#.

Ecco un esempio:

// esempi/cap-3/enumeration-script.scala

object Breed extends Enumeration {
  val doberman = Value("Dobermann")
  val yorkie = Value("Yorkshire Terrier")
  val scottie = Value("Scottish Terrier")
  val dane = Value("Alano")
  val portie = Value("Cane d'acqua portoghese")
}

// stampa una lista di razze con il loro identificatore
println("ID\tRazza")
for (breed <- Breed) println(breed.id + "\t" + breed)

// stampa una lista di razze Terrier
println("\nSolo i Terrier:")
Breed.filter(_.toString.endsWith("Terrier")).foreach(println)

Eseguendolo, otterrete l’uscita seguente:

ID      Razza
0       Dobermann
1       Yorkshire Terrier
2       Scottish Terrier
3       Alano
4       Cane d'acqua portoghese

Solo i Terrier:
Yorkshire Terrier
Scottish Terrier
Possiamo vedere che il nostro tipo enumerato Breed contiene diverse variabili di tipo Value, come nell’esempio seguente.
val doberman = Value("Dobermann")

Ogni dichiarazione in realtà invoca un metodo chiamato Value che prende una stringa come argomento. Usiamo questo metodo per assegnare a ogni enumerazione un nome di razza completo, che il metodo Value.toString ha usato come valore di ritorno nell’uscita appena vista.

Notate che non c’è conflitto di nomi tra il tipo e il metodo che sono entrambi chiamati Value. Ci sono altre versioni sovraccaricate del metodo Value. Una di queste non accetta argomenti, un’altra accetta un identificatore numerico di tipo Int e un’altra ancora accetta un Int e una String. Questi metodi Value restituiscono un oggetto Value e aggiungono il valore alla collezione di valori del tipo enumerato.

In effetti, la classe Enumeration di Scala supporta i soliti metodi per lavorare con le collezioni, così possiamo facilmente iterare attraverso le razze con un ciclo for e filtrarle per nome. L’uscita appena vista dimostra anche che a ogni Value in un’enumerazione viene automaticamente assegnato un identificatore numerico, a meno che non invochiate uno dei metodi Value in cui specificate esplicitamente il vostro identificatore.

Vorrete spesso dare nomi leggibili alle vostre enumerazioni. Tuttavia, a volte potreste non averne bisogno. Ecco un altro esempio di enumerazione adattato dalla documentazione Scaladoc per Enumeration.

// esempi/cap-3/days-enumeration-script.scala

object WeekDay extends Enumeration {
  type WeekDay = Value
  val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
}
import WeekDay._

def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)

WeekDay filter isWorkingDay foreach println

L’esecuzione di questo script tramite scala produce la seguente uscita.

Main$$anon$1$WeekDay(0)
Main$$anon$1$WeekDay(1)
Main$$anon$1$WeekDay(2)
Main$$anon$1$WeekDay(3)
Main$$anon$1$WeekDay(4)

Quando non vengono assegnati nomi evitando di usare uno dei metodi Value che prendono una stringa come argomento, Value.toString stampa il nome di tipo che viene prodotto dal compilatore, insieme al valore dell’identificatore che è stato generato automaticamente. (In questo caso, Scala 2.8 stampa i nomi dei valori di enumerazione: Mon, Tue, &c.)

Notate che importando WeekDay._ abbiamo reso visibili tutti i valori di enumerazione, come Mon, Tue, &c. Altrimenti, avreste dovuto scrivere WeekDay.Mon, WeekDay.Tue, &c.

In più, l’importazione ha reso visibile l’alias di tipo type WeekDay = Value, che abbiamo usato come tipo per il parametro del metodo isWorkingDay. Senza un alias di tipo come questo, il metodo andrebbe dichiarato come def isWorkingDay(d: WeekDay.Value).

Dato che le enumerazioni di Scala sono solo normali oggetti, potreste usare qualsiasi oggetto che definisca diversi campi val per denotare i “valori di enumerazione”. Tuttavia, estendere Enumeration presenta diversi vantaggi: i valori vengono gestiti automaticamente come collezioni su cui potete iterare (come nei nostri esempi) e un identificatore numerico unico viene assegnato automaticamente a ogni valore.

Le classi case (si veda la sezione Classi case nel capitolo 6) vengono spesso usate al posto delle enumerazioni in Scala, perché i loro “casi d’uso” coinvolgono spesso il pattern matching. Riparleremo di questo argomento nella sezione Un confronto tra enumerazioni e pattern matching del capitolo 13.

Riepilogo, e poi?

Abbiamo trattato molti argomenti in questo capitolo. Abbiamo imparato quanto può essere flessibile la sintassi di Scala e come essa faciliti la creazione di linguaggi domain-specific. Poi abbiamo esplorato i miglioramenti apportati da Scala ai costrutti di ciclo e alle espressioni condizionali. Abbiamo sperimentato diverse applicazioni del pattern matching, una potente estensione della comune istruzione case/switch. Infine, abbiamo imparato come incapsulare valori nelle enumerazioni.

Ora dovreste essere pronti per leggere una certa quantità di codice Scala, ma ci sono molte altre funzioni del linguaggio ancora da aggiungere alla vostra cintura degli attrezzi. Nei prossimi quattro capitoli esploreremo l’approccio di Scala alla programmazione orientata agli oggetti, cominciando con i tratti.


  1. [NdT] In realtà, le specifiche si leggono come frasi in “linguaggio naturale” solo se scrivete le stringhe di descrizione in inglese, poiché i vari metodi di Specs (come should, in e mustEqual) sono stati implementati con nomi in inglese. Se scrivete le descrizioni degli scenari in italiano, la fluidità di lettura risulterà compromessa.

© 2008–9 O’Reilly Media
© 2009–10 Giulio Piancastelli per la traduzione italiana