Voi siete qui: Inizio Programmare in Scala

Scrivere meno, fare di più

In questo capitolo

Abbiamo concluso l’ultimo capitolo con alcuni esempi “stuzzicanti” di codice Scala. Questo capitolo considera gli usi di Scala che promuovono codice sintetico e flessibile. Esamineremo l’organizzazione di file e package, i meccanismi per importare altri tipi, le dichiarazioni di variabile, varie convenzioni sintattiche e alcuni altri concetti. Metteremo in risalto il modo in cui la sintassi concisa di Scala vi aiuta a lavorare meglio e più velocemente.

La sintassi di Scala è particolarmente utile per scrivere script. Non è obbligatorio separare i passi di compilazione ed esecuzione per quei semplici programmi che hanno poche dipendenze nei confronti di librerie aggiuntive rispetto a quelle fornite da Scala. Potete compilare ed eseguire questi programmi in un unico passo con il comando scala. Se avete scaricato il codice di esempio del libro, molti degli esempi più piccoli possono essere eseguiti tramite il comando scala, invocando scala nomefile.scala. Si vedano i file README.txt negli esempi di codice del singolo capitolo per maggiori dettagli. Si veda anche la sezione Strumenti a riga di comando nel capitolo 14 per maggiori informazioni su come usare il comando scala.

Punti e virgola

Potreste avere già notato che ci sono davvero pochi punti e virgola negli esempi di codice del capitolo precedente. Potete usare i punti e virgola per separare istruzioni ed espressioni, come in Java, C, PHP e simili. Nella maggior parte dei casi, tuttavia, Scala si comporta come molti linguaggi di scripting che trattano la fine di una riga come la fine di un’istruzione o di un’espressione. Quando le istruzioni o le espressioni sono troppo lunghe per stare su una sola riga, di solito Scala è in grado di dedurre se state proseguendo sulla riga successiva, come mostrato in questo esempio.

// esempi/cap-2/semicolon-example-script.scala

// Il segno di uguale al termine di una riga
// indica altro codice nella riga successiva
def equalsign = {
  val reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine =
    "questo è un nome di valore davvero lungo"

  println(reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine)
}

// Una parentesi graffa aperta al termine di una riga
// indica altro codice nella riga successiva
def equalsign2(s: String) = {
  println("equalsign2: " + s)
}

// Una virgola, un operatore, &c. al termine di una riga
// indicano altro codice nella riga successiva
def commas(s1: String,
           s2: String) = {
  println("commas: " + s1 +
          ", " + s2)
}

Quando volete mettere più istruzioni o espressioni sulla stessa riga, potete usare i punti e virgola per separarle. Abbiamo usato questa tecnica nell’esempio di ShapeDrawingActor nella sezione Un assaggio di concorrenza del capitolo 1.

case "exit" => println("termino..."); exit

Questo codice si potrebbe anche scrivere in questo modo

…
case "exit" =>
  println("termino...")
  exit
…

Potreste chiedervi perché non avete bisogno di parentesi graffe ({…}) attorno alle due istruzioni dopo la riga che contiene case … =>. Se volete potete metterle, ma il compilatore sa che avete raggiunto la fine del “blocco” quando trova la clausola case successiva o la parentesi graffa (}) che conclude il blocco contenente tutte le clausole case.

Omettere i punti e virgola opzionali significa digitare meno caratteri e rendere meno ridondante il vostro codice. Collocare ogni istruzione sulla propria riga aumenta la leggibilità del vostro codice.

Dichiarazioni di variabile

Scala vi consente di decidere se una variabile è immutabile (a sola lettura) oppure no (a lettura e scrittura) nel momento in cui la dichiarate. Una “variabile” immutabile si dichiara con la parola chiave val (pensate a un oggetto valore).

val array: Array[String] = new Array(5)

Per essere più precisi, il riferimento array non può essere modificato per puntare a un’istanza di Array differente, ma l’array può essere modificato, come si vede nella seguente sessione interattiva di scala.

scala> val array: Array[String] = new Array(5)
array: Array[String] = Array(null, null, null, null, null)

scala> array = new Array(2)
<console>:5: error: reassignment to val
       array = new Array(2)
             ^

scala> array(0) = "Ciao"

scala> array
res3: Array[String] = Array(Ciao, null, null, null, null)

scala>

Una variabile immutabile di tipo val deve essere inizializzata, cioè definita, nel momento in cui viene dichiarata.

Una variabile mutabile si dichiara con la parola chiave var.

scala> var stockPrice: Double = 100.
stockPrice: Double = 100.0

scala> stockPrice = 10.
stockPrice: Double = 10.0

scala>

Scala vi obbliga a inizializzare anche le variabili di tipo var nel momento in cui le dichiarate. Potete assegnare un nuovo valore a una variabile di questo tipo ogni volta che volete. Sempre per essere precisi, il riferimento stockPrice può essere modificato per puntare a un oggetto Double differente (per esempio, .10). In questo caso, l’oggetto a cui stockPrice fa riferimento non può essere modificato perché in Scala le istanze di Double sono immutabili.

Ci sono alcune eccezioni alla regola che prescrive di inizializzare le variabili di tipo val e var al momento della dichiarazione. Entrambe le parole chiave possono essere usate con i parametri di un costruttore; in questo caso, le variabili mutabili o immutabili specificate saranno inizializzate quando un oggetto viene istanziato. Entrambe le parole chiave possono essere usate per dichiarare variabili “astratte” (non inizializzate) nei tipi astratti. In più, i tipi derivati possono ridefinire le variabili di tipo val dichiarate nei tipi genitore. Parleremo di queste eccezioni nel capitolo 5.

Scala vi incoraggia a usare valori immutabili ovunque sia possibile. Come vedrete, questo migliora la progettazione orientata agli oggetti ed è coerente con i principi della programmazione funzionale “pura”. Potreste doverci fare l’abitudine, ma scoprirete di fare affidamento sul vostro codice in un modo del tutto nuovo quando è scritto in uno stile immutabile.

Le parole chiave var e val specificano solo se il riferimento può essere modificato per puntare a un oggetto differente (var) oppure no (val). Non specificano se l’oggetto puntato è mutabile oppure no.

Dichiarazioni di metodo

Nel capitolo 1 abbiamo visto diversi esempi di come si definisce un metodo, che è una funzione membro di una classe. Le definizioni di metodo cominciano con la parola chiave def seguita dal nome del metodo, da liste opzionali di argomenti, da un carattere di due punti “:” e dal tipo di ritorno del metodo, da un segno di uguale “=” e infine dal corpo del metodo. I metodi sono dichiarati implicitamente “astratti” se omettete il segno di uguale e il corpo del metodo. Di conseguenza, il tipo che li contiene diventa astratto. Discuteremo i tipi astratti in maggior dettaglio nel capitolo 5.

Abbiamo detto “liste opzionali di argomenti” per indicare che Scala vi permette di definire più di una lista di argomenti per un metodo. Questo è necessario per il currying dei metodi, che discuteremo nella sezione Currying del capitolo 8. È anche molto utile per definire i vostri linguaggi domain-specific (DSL), come vedremo nel capitolo 11. Notate che ogni lista di argomenti è racchiusa tra parentesi e gli argomenti sono separati da virgole.

Se il corpo di un metodo contiene più espressioni, dovete circondarlo con una coppia di parentesi graffe {…}. Potete omettere le parentesi se il corpo del metodo contiene una sola espressione.

Argomenti con nome e argomenti predefiniti per i metodi (Scala 2.8)

Molti linguaggi vi permettono di specificare valori predefiniti per alcuni o per tutti gli argomenti di un metodo. Considerate lo script seguente, che contiene un oggetto StringUtil progettato per unire le stringhe contenute in una lista tramite un separatore indicato dall’utente.

// esempi/cap-2/string-util-v1-script.scala
// StringUtil versione 1.

object StringUtil {
  def joiner(strings: List[String], separator: String): String =
    strings.mkString(separator)

  def joiner(strings: List[String]): String = joiner(strings, " ")
}
import StringUtil._  // Importa i metodi di unione.

println(joiner(List("Programmare", "in", "Scala")))

In realtà ci sono due metodi joiner “sovraccaricati”, di cui il secondo usa un singolo spazio come separatore “predefinito”. Avere due metodi sembra uno spreco; sarebbe meglio se potessimo eliminare il secondo metodo joiner e dichiarare che l’argomento separator del primo metodo joiner ha un valore predefinito. Nella versione 2.8 di Scala potete farlo.

// esempi/cap-2/string-util-v2-v28-script.scala
// StringUtil versione 2, solo per Scala v2.8.

object StringUtil {
  def joiner(strings: List[String], separator: String = " "): String =
    strings.mkString(separator)
}
import StringUtil._  // Importa il metodo di unione.

println(joiner(List("Programmare", "in", "Scala")))

Esiste un’altra alternativa per le versioni precedenti di Scala. Potete usare gli argomenti impliciti, che discuteremo nella sezione Parametri di funzione impliciti del capitolo 8.

La versione 2.8 di Scala offre un altro miglioramento per le liste di argomenti dei metodi: gli argomenti con nome. Potete effettivamente scrivere l’ultima riga dell’esempio precedente in molti modi. Tutte le invocazioni di println che seguono sono funzionalmente equivalenti.

println(joiner(List("Programmare", "in", "Scala")))
println(joiner(strings=List("Programmare", "in", "Scala")))
println(joiner(List("Programmare", "in", "Scala"), " "))  // #1
println(joiner(List("Programmare", "in", "Scala"), separator=" "))  // #2
println(joiner(strings=List("Programmare", "in", "Scala"), separator=" "))

Perché questo è utile? Prima di tutto, se scegliete nomi significativi per gli argomenti del metodo, tutte le invocazioni di quel metodo descrivono ogni argomento con il suo nome. Per esempio, confrontate le due righe con i commenti #1 e #2. Nella prima riga lo scopo del secondo argomento " " potrebbe non essere ovvio. Nel secondo caso, forniamo il nome separator che suggerisce lo scopo dell’argomento.

Il secondo vantaggio è quello di poter specificare i parametri in qualsiasi ordine quando li indicate con il loro nome. Combinando questa caratteristica con i valori predefiniti, potete scrivere codice come quello che segue.

// esempi/cap-2/user-profile-v28-script.scala
// Solo per Scala v2.8.

object OptionalUserProfileInfo {
  val UnknownLocation = ""
  val UnknownAge = -1
  val UnknownWebSite = ""
}

class OptionalUserProfileInfo(
  location: String = OptionalUserProfileInfo.UnknownLocation,
  age: Int         = OptionalUserProfileInfo.UnknownAge,
  webSite: String  = OptionalUserProfileInfo.UnknownWebSite)

println(new OptionalUserProfileInfo)
println(new OptionalUserProfileInfo(age=29))
println(new OptionalUserProfileInfo(age=29, location="Terra"))

OptionalUserProfileInfo rappresenta tutte le informazioni “opzionali” del profilo di un utente nella vostra prossima applicazione di social network nel web 2.0. La classe definisce valori predefiniti per tutti i propri campi. Lo script crea istanze con zero o più parametri con nome. L’ordine di questi parametri è arbitrario.

Gli esempi che abbiamo mostrato usano valori costanti come valori predefiniti. La maggior parte dei linguaggi che supportano i valori predefiniti degli argomenti consente di usare solo costanti o altri valori che possono essere determinati durante il riconoscimento del codice sorgente. Tuttavia, in Scala, qualsiasi espressione può essere usata come valore predefinito, purché possa essere compilata nel punto in cui viene usata. Per esempio, un’espressione non potrebbe fare riferimento al campo di un’istanza che verrà calcolato nel corpo della classe o dell’oggetto, ma potrebbe invocare un metodo su un oggetto singleton.

Una limitazione correlata è quella per cui l’espressione predefinita di un parametro non può fare riferimento a un altro parametro nella lista, a meno che il parametro riferito non appaia prima nella lista e che i parametri siano applicati parzialmente, un concetto che discuteremo nella sezione Currying del capitolo 8.

Infine, per un altro vincolo sui parametri con nome, in una invocazione di metodo tutti i parametri che seguono un parametro di cui avete fornito il nome devono essere a loro volta parametri con nome. Per esempio, new OptionalUserProfileInfo(age=29, "Terra") non verrebbe compilato perché il secondo argomento non è passato con il proprio nome.

Vedremo un altro esempio utile di argomenti con nome e argomenti predefiniti quando discuteremo le classi case nella sezione Classi case del capitolo 6.

Annidare le definizioni di metodo

Le definizioni di metodo possono anche essere annidate. In questa implementazione della funzione fattoriale usiamo la tecnica convenzionale di invocare un secondo metodo annidato per effettuare il calcolo.

// esempi/cap-2/factorial-script.scala

def factorial(i: Int): Int = {
  def fact(i: Int, accumulator: Int): Int = {
    if (i <= 1)
      accumulator
    else
      fact(i - 1, i * accumulator)
  }

  fact(i, 1)
}

println(factorial(0))
println(factorial(1))
println(factorial(2))
println(factorial(3))
println(factorial(4))
println(factorial(5))

Il secondo metodo invoca se stesso ricorsivamente, passando un parametro accumulator in cui viene “accumulato” il risultato del calcolo. Notate che, quando il contatore i raggiunge 1, restituiamo il valore accumulato. (Stiamo ignorando gli interi negativi non validi. La funzione in realtà restituisce 1 per i < 0.) Dopo aver definito il metodo annidato, factorial lo invoca con il valore i che gli viene passato e con il valore iniziale 1 per l’accumulatore.

Come accade per le dichiarazioni di variabili locali in molti linguaggi, un metodo annidato è visibile solamente all’interno del metodo che lo include. Se provate a invocare fact al di fuori di factorial, otterrete un errore di compilazione.

Avete notato che abbiamo usato due volte i come nome di parametro, prima nel metodo factorial e poi nel metodo annidato fact? Come accade in molti linguaggi, l’uso di i come nome di parametro per fact “oscura” l’uso più esterno di i come nome di parametro per factorial. Questo non ci crea problemi, perché non abbiamo bisogno del valore più esterno di i all’interno di fact; lo usiamo solo la prima volta che invochiamo fact, alla fine di factorial.

E se abbiamo bisogno di usare una variabile che è definita al di fuori di una funzione annidata? Considerate questo esempio artificioso.

// esempi/cap-2/count-to-script.scala

def countTo(n: Int):Unit = {
  def count(i: Int): Unit = {
    if (i <= n) {
      println(i)
      count(i + 1)
    }
  }
  count(1)
}

countTo(5)

Notate che il metodo count annidato usa il valore n che viene passato come parametro a countTo. Non c’è alcun bisogno di passare n come argomento a count; n è visibile perché count è annidato all’interno di countTo.

La dichiarazione di un campo (variabile membro) può essere qualificata con parole chiave che ne indicano la visibilità, esattamente come accade in Java e C#. Similmente, la dichiarazione di metodi non annidati può essere qualificata con le stesse parole chiave. Discuteremo le regole di visibilità e le relative parole chiave nella sezione Regole di visibilità del capitolo 5.

Inferire le informazioni di tipo

I linguaggi staticamente tipati possono essere piuttosto prolissi. Considerate questa tipica dichiarazione in Java.

import java.util.Map;
import java.util.HashMap;
…
Map<Integer, String> intToStringMap = new HashMap<Integer, String>();

Dobbiamo specificare i parametri di tipo <Integer, String> due volte. (Scala usa il termine annotazioni di tipo per le dichiarazioni di tipo esplicite come HashMap<Integer, String>.)

Scala supporta l’inferenza di tipo (si veda, per esempio, [TypeInference] e [Pierce2002]). Il compilatore del linguaggio può dedurre una certa quantità di informazioni di tipo dal contesto, senza che vi siano annotazioni di tipo esplicite. Ecco la stessa dichiarazione riscritta in Scala, con le informazioni di tipo inferite.

import java.util.Map
import java.util.HashMap
…
val intToStringMap: Map[Integer, String] = new HashMap

Ricordatevi che, come avete visto nel capitolo 1, Scala usa le parentesi quadre ([…]) per i parametri di tipo generici. Specifichiamo Map[Integer, String] sul lato sinistro del segno di uguale. (Manteniamo i tipi di Java in questo esempio.) Sul lato destro istanziamo HashMap, il tipo che vogliamo realmente, ma non siamo obbligati a ripetere i parametri di tipo.

Per completezza, supponiamo che non ci interessi se l’istanza è di tipo Map (il tipo di interfaccia di Java). Potrebbe essere di tipo HashMap, per quel che ce ne importa.

import java.util.Map
import java.util.HashMap
…
val intToStringMap2 = new HashMap[Integer, String]

Questa dichiarazione non richiede alcuna annotazione di tipo sul lato sinistro perché tutte le informazioni di tipo necessarie si trovano sul lato destro. Il compilatore rende il tipo di intToStringMap2 automaticamente uguale a HashMap[Integer, String].

L’inferenza di tipo viene usata anche per i metodi. Nella maggior parte dei casi, il tipo di ritorno del metodo può essere inferito, quindi i due punti “:” e il tipo di ritorno si possono omettere. Tuttavia, le annotazioni di tipo sono obbligatorie per tutti i parametri dei metodi.

I linguaggi puramente funzionali come Haskell (si veda per esempio [O’Sullivan2009]) usano algoritmi di inferenza di tipo come l’algoritmo Hindley-Milner (si veda [Spiewak2008] per una spiegazione facilmente digeribile). Il codice scritto in questi linguaggi richiede le annotazioni di tipo più raramente rispetto a Scala, perché l’inferenza di tipo in Scala deve anche tenere conto della tipizzazione orientata agli oggetti. Quindi, Scala ha bisogno di più annotazioni di tipo rispetto a linguaggi come Haskell. Ecco un riepilogo delle regole che spiegano quando le annotazioni di tipo esplicite sono obbligatorie in Scala.

Il tipo Any è la radice della gerarchia di tipi di Scala (si veda la sezione La gerarchia di tipi di Scala nel capitolo 7 per maggiori dettagli). Se un blocco di codice restituisce un valore di tipo Any in maniera inaspettata, è probabile che l’algoritmo di inferenza di tipo non sia riuscito a capire quale tipo restituire e quindi abbia scelto il tipo più generico possibile.

Diamo un’occhiata ad alcuni esempi in cui le dichiarazioni esplicite del tipo di ritorno di un metodo sono obbligatorie. Nello script seguente, il metodo upCase contiene un’istruzione di ritorno condizionale per le stringhe di lunghezza zero.

// esempi/cap-2/method-nested-return-script.scala
// ERRORE: non verrà compilato fino a quando non mettete
//         un tipo di ritorno uguale a String per upCase.

def upCase(s: String) = {
  if (s.length == 0)
    return s   // ERRORE - obbliga a dichiarare il tipo di ritorno di upCase.
  else
    s.toUpperCase()
}

println(upCase(""))
println(upCase("Ciao"))

L’esecuzione di questo script vi darà il seguente errore.

... 6: error: method upCase has return statement; needs result type
        return s
         ^

Potete correggere questo errore modificando la prima riga del metodo nel modo seguente.

def upCase(s: String): String = {

In realtà, per questo particolare script, è possibile una correzione alternativa che prevede di rimuovere la parola chiave return dalla riga. La parola chiave non è necessaria per il corretto funzionamento del codice, ma illustra il nostro punto.

Anche i metodi ricorsivi richiedono un tipo di ritorno esplicito. Ricordatevi il nostro metodo factorial nella sezione Annidare le definizioni di metodo di questo capitolo. Proviamo a rimuovere il tipo di ritorno : Int per il metodo annidato fact.

// esempi/cap-2/method-recursive-return-script.scala
// ERRORE: non verrà compilato fino a quando non mettete
           un tipo di ritorno uguale a Int per fact.

def factorial(i: Int) = {
  def fact(i: Int, accumulator: Int) = {
    if (i <= 1)
      accumulator
    else
      fact(i - 1, i * accumulator)  // ERRORE
  }

  fact(i, 1)
}

Ora lo script non viene compilato.

... 9: error: recursive method fact needs result type
            fact(i - 1, i * accumulator)
             ^

I metodi sovraccaricati possono talvolta richiedere un tipo di ritorno esplicito. Quando uno di questi metodi ne chiama un altro, dobbiamo aggiungere un tipo di ritorno a quello che effettua la chiamata, come in questo esempio.

// esempi/cap-2/method-overloaded-return-script.scala
// StringUtil versione 1 (con un errore di compilazione).
// ERRORE: non verrà compilato; necessita di un tipo di
//         ritorno uguale a String per il secondo joiner.

object StringUtil {
  def joiner(strings: List[String], separator: String): String =
    strings.mkString(separator)

  def joiner(strings: List[String]) = joiner(strings, " ")  // ERRORE
}
import StringUtil._  // Importa i metodi di unione.

println(joiner(List("Programmare", "in", "Scala")))

I due metodi joiner concatenano le stringhe contenute in una lista. Il primo metodo prende come argomento anche la stringa separatrice. Il secondo metodo chiama il primo con un separatore “predefinito” uguale a un singolo spazio.

Se eseguite questo script, ottenete l’errore seguente.

... 9: error: overloaded method joiner needs result type
def joiner(strings: List[String]) = joiner(strings, " ")
Dato che il secondo metodo joiner chiama il primo, ha bisogno di un tipo di ritorno String esplicito, che dovrebbe apparire in questo modo.
  def joiner(strings: List[String]): String = joiner(strings, " ")

L’ultimo scenario, in cui il compilatore inferisce un tipo di ritorno più generale di quello che vi aspettate, può essere astruso. Di solito vedete questo errore quando assegnate un valore restituito da una funzione a una variabile con un tipo più specifico. Per esempio, vi aspettavate String ma la funzione ha inferito Any come tipo dell’oggetto restituito. Vediamo un esempio artificioso che riflette un errore legato all’eventualità di questo scenario.

// esempi/cap-2/method-broad-inference-return-script.scala
// ERRORE: non verrà compilato. Il metodo in realtà
           restituisce List[Any], che è troppo "ampio".

def makeList(strings: String*) = {
  if (strings.length == 0)
    List(0)  // #1
  else
    strings.toList
}

val list: List[String] = makeList()  // ERRORE

L’esecuzione di questo script restituisce l’errore seguente.

...11: error: type mismatch;
 found   : List[Any]
 required: List[String]
val list: List[String] = makeList()
                          ^

Volevamo che makeList restituisse List[String], ma quando strings.length è uguale a zero abbiamo restituito List(0) “supponendo” in maniera scorretta che questa espressione fosse il modo giusto di creare una lista vuota. In effetti, abbiamo restituito un’istanza di List[Int] con un elemento, 0, mentre avremmo dovuto restituire List(). Dato che l’espressione else restituisce List[String] come tipo del risultato di strings.toList, il tipo di ritorno inferito per il metodo è il supertipo comune di List[Int] e List[String] più vicino, cioè List[Any]. Notate che l’errore di compilazione non avviene nella definizione di funzione, ma lo vediamo solo quando cerchiamo di assegnare il valore restituito da makeList a una variabile di tipo List[String].

In questo caso, la soluzione è correggere l’errore. In alternativa, quando non c’è un errore, è possibile che il compilatore abbia solo bisogno di essere aiutato con una dichiarazione esplicita del tipo di ritorno. Esaminate il metodo che sembra restituire il tipo inaspettato. La nostra esperienza ci dice che spesso scoprirete di aver modificato quel metodo (o un altro metodo nel percorso di chiamata) in un modo tale da indurre il compilatore a inferire un tipo di ritorno più generale del necessario. In questo caso, aggiungete il tipo di ritorno esplicito.

Un altro modo per evitare questi problemi è dichiarare sempre il tipo di ritorno dei metodi, in particolare quando definite i metodi di una API pubblica. Rivisitiamo il nostro esempio di StringUtil e vediamo perché le dichiarazioni esplicite sono una buona idea (adattato da [Smith2009a]).

Ecco ancora la nostra “APIStringUtil con un nuovo metodo chiamato toCollection.

// esempi/cap-2/string-util-v3.scala
// StringUtil versione 3 (per tutte le versioni di Scala).

object StringUtil {
  def joiner(strings: List[String], separator: String): String =
    strings.mkString(separator)

  def joiner(strings: List[String]): String = strings.mkString(" ")

  def toCollection(string: String) = string.split(' ')
}

Il metodo toCollection divide una stringa in corrispondenza degli spazi e restituisce un Array contenente le sottostringhe. Il tipo di ritorno viene inferito, e come vedremo questo è un potenziale problema. Il metodo è piuttosto artificioso, ma ci servirà per illustrare il nostro punto. Ecco un cliente di StringUtil che usa questo metodo.

// esempi/cap-2/string-util-client.scala

import StringUtil._

object StringUtilClient {
  def main(args: Array[String]) = {
    args foreach { s => toCollection(s).foreach { x => println(x) } }
  }
}

Se compilate questi file con scala, potete eseguire il programma cliente come segue.

$ scala -cp … StringUtilClient "Programmare in Scala"
Programmare
in
Scala

Per l’argomento -cp … del percorso delle classi, usate la directory in cui scalac ha generato i file di classe, che per default è la directory corrente (cioè, usate -cp .). Se avete usato il procedimento di assemblaggio degli esempi di codice scaricati, i file di classe sono stati generati nella directory build (usando scalac -d build …). In questo caso, usate -cp build.

A questo punto è tutto a posto, ma ora immaginate che la vostra base di codice sia cresciuta e che StringUtil e i suoi clienti vengano compilati separatamente e distribuiti in file JAR separati. Immaginate anche che i manutentori di StringUtil decidano di restituire una lista invece del tipo predefinito.

object StringUtil {
  …

  def toCollection(string: String) = string.split(' ').toList  // modificato!
}

L’unica differenza è l’invocazione finale di toList che converte l’istanza calcolata di Array in un’istanza di List. Ricompilate StringUtil e ricreatene il JAR, poi eseguite lo stesso cliente senza prima ricompilarlo.

$ scala -cp … StringUtilClient "Programmare in Scala"
java.lang.NoSuchMethodError: StringUtil$.toCollection(…
  at StringUtilClient$$anonfun$main$1.apply(string-util-client.scala:6)
  at StringUtilClient$$anonfun$main$1.apply(string-util-client.scala:6)
…

Che cosa è successo? Quando il cliente è stato compilato, StringUtil.toCollection restituiva un Array. Poi, toCollection è stato modificato per restituire il tipo List. In entrambe le versioni, il valore di ritorno del metodo veniva inferito. Di conseguenza, anche il cliente avrebbe dovuto essere ricompilato.

Tuttavia, se fosse stato dichiarato un tipo di ritorno esplicito come Seq, che è un genitore sia di Array sia di List, allora la modifica all’implementazione non avrebbe costretto il cliente alla ricompilazione.

Quando sviluppate API compilate separatamente dai loro clienti, dichiarate esplicitamente i tipi di ritorno dei metodi e usate il tipo di ritorno più generale che potete. Questo è particolarmente importante quando le API dichiarano metodi astratti (si veda per esempio il capitolo 4).

C’è un altro scenario a cui dovete fare attenzione quando usate dichiarazioni di collezioni come val map = Map(), illustrato in questo esempio.

val map = Map()

map.update("libro", "Programmare in Scala")
... 3: error: type mismatch;
 found   : java.lang.String("libro")
 required: Nothing
map.update("libro", "Programmare in Scala")
            ^

Che cosa è successo? I parametri di tipo del tipo generico Map sono stati inferiti come [Nothing, Nothing] quando la mappa è stata creata. (Discuteremo Nothing nella sezione La gerarchia di tipi di Scala del capitolo 7, ma il suo nome è suggestivo!) Abbiamo tentato di inserire una coppia chiave-valore incompatibile di tipo String-String. Quella mappa non porta in nessun luogo! La soluzione consiste nel parametrizzare la dichiarazione iniziale, come in val map = Map[String, String](), o nello specificare i valori iniziali in modo che i parametri di tipo vengano inferiti, come in val map = Map("libro" -> "Programmare in Scala").

Infine, a volte i tipi di ritorno inferiti possono comportarsi in maniera astrusa causando risultati inaspettati e sconcertanti [ScalaTips]. Considerate come esempio la sessione scala seguente.

scala> def double(i: Int) { 2 * i }
double: (Int)Unit

scala> println(double(2))
()

Perché il secondo comando ha stampato () invece di 4? Esaminate attentamente ciò che l’interprete scala ha indicato come valore restituito dal primo comando, double: (Int)Unit. Abbiamo definito un metodo chiamato double che prende un argomento di tipo Int e restituisce Unit. Il metodo non restituisce un Int come ci saremmo aspettati.

La causa di questo comportamento inaspettato è la mancanza di un segno di uguale nella definizione di metodo. Ecco la definizione che effettivamente intendevamo.

scala> def double(i: Int) = { 2 * i }
double: (Int)Int

scala> println(double(2))
4

Notate il segno di uguale prima del corpo di double. Ora l’uscita ci dice che abbiamo definito double in modo che restituisca un Int e il secondo comando fa quello che ci aspettiamo.

C’è una ragione per questo comportamento. Scala considera un metodo con il segno di uguale prima del corpo come una definizione di funzione, e nella programmazione funzionale le funzioni restituiscono sempre un valore. D’altra parte, quando Scala vede il corpo di un metodo senza il segno di uguale a precederlo, presume che il programmatore intenda il metodo come la definizione di una “procedura”, progettata per sfruttarne solo gli effetti collaterali e per restituire Unit. In pratica, è più probabile che il programmatore abbia semplicemente dimenticato di inserire il segno di uguale!

Quando il tipo di ritorno di un metodo viene inferito e non usate un segno di uguale prima della parentesi graffa aperta per il corpo del metodo, Scala inferisce Unit come tipo di ritorno, anche quando l’ultima espressione nel metodo è un valore di un altro tipo.

A proposito, da dove è venuta fuori quella coppia di parentesi () che è stata stampata prima che correggessimo l’errore? È il vero nome dell’istanza singleton del tipo Unit! (Questo nome è una convenzione derivata dalla programmazione funzionale.)

Letterali

Spesso, un nuovo oggetto viene inizializzato con un valore letterale, come in val book = "Programmare in Scala". Ora parleremo dei tipi di valori letterali supportati da Scala, limitandoci ai letterali che sono anche lessemi. Parleremo della sintassi letterale per funzioni (usate come valori, non come metodi), tuple e certi tipi come liste e mappe man mano che arriveremo a parlare di quei tipi.

Letterali interi

I letterali interi possono essere espressi in decimale, esadecimale, oppure ottale. I dettagli sono riepilogati nella tabella 2.1.

Tabella 2.1. Letterali interi.

TipoFormatoEsempi

Decimale

0 oppure una cifra diversa da zero seguita da zero o più cifre (0-9)

0, 1, 321

Esadecimale

0x seguito da una o più cifre esadecimali (0-9, A-F, a-f)

0xFF, 0x1a3b

Ottale

0 seguito da una o più cifre ottali (0-7)

013, 077

Per i letterali Long è necessario aggiungere il carattere L o l alla fine del letterale; altrimenti, viene usato un Int. I valori validi per un letterale intero sono legati al tipo della variabile a cui il valore viene assegnato. La tabella 2.2 definisce gli estremi, che sono inclusi.

Tabella 2.2. Intervalli di valori permessi per i letterali interi (gli estremi sono inclusi).

Tipo obiettivoMinimo (incluso)Massimo (incluso)

Long

−263

263 - 1

Int

−231

231 - 1

Short

−215

215 - 1

Char

0

216 - 1

Byte

−27

27 - 1

L’uso di un numero letterale intero al di fuori di questi intervalli provoca un errore a tempo di compilazione, come negli esempi seguenti.

scala > val i = 12345678901234567890
<console>:1: error: integer number too large
       val i = 12345678901234567890
scala> val b: Byte = 128
<console>:4: error: type mismatch;
 found   : Int(128)
 required: Byte
       val b: Byte = 128
                     ^

scala> val b: Byte = 127
b: Byte = 127

Letterali in virgola mobile

I letterali in virgola mobile sono espressioni con zero o più cifre, seguite da un punto ., seguito da zero o più cifre. Se non ci sono cifre prima del punto, e quindi il numero è minore di 1.0, allora ci deve essere almeno una cifra dopo il punto. Per i letterali Float è necessario aggiungere il carattere F o f alla fine del letterale; altrimenti, viene usato un Double. L’aggiunta di D o d per ottenere un Double è opzionale.

I letterali in virgola mobile possono essere espressi con o senza gli esponenti. Il formato della parte esponenziale è e o E, seguita da un segno + o - opzionale, seguito da una o più cifre.

Ecco alcuni esempi di letterali in virgola mobile.

0.
.0
0.0
3.
3.14
.14
0.14
3e5
3E5
3.E5
3.e5
3.e+5
3.e-5
3.14e-5
3.14e-5f
3.14e-5F
3.14e-5d
3.14e-5D

La classe Float comprende solo valori in virgola mobile a singola precisione (32 bit) che seguono lo standard IEEE 754. La classe Double comprende valori in virgola mobile a doppia precisione (64 bit) che seguono lo standard IEEE 754.

Per evitare ambiguità sintattiche, deve sempre esserci almeno uno spazio dopo un letterale in virgola mobile se questo è seguito da un lessema che comincia con una lettera. In più, l’espressione 1.toString restituisce il valore intero 1 sotto forma di stringa, mentre 1. toString sfrutta la notazione operazionale per invocare toString sul letterale in virgola mobile 1..

Letterali booleani

I letterali booleani sono true e false. Il tipo della variabile a cui vengono assegnati verrà inferito come Boolean.

scala> val b1 = true
b1: Boolean = true

scala> val b2 = false
b2: Boolean = false

Letterali carattere

Un letterale carattere è un carattere Unicode stampabile, oppure una sequenza di escape, racchiuso tra apici. Un carattere con un valore Unicode compreso tra 0 e 255 può anche essere rappresentato come una sequenza di escape ottale, cioè come un carattere di barra inversa \ seguito da una sequenza di non più di tre cifre ottali. In un letterale carattere o stringa, una barra inversa che non comincia una sequenza di escape valida provoca un errore di compilazione.

Ecco alcuni esempi.

'A'
'\u0041'  // 'A' in Unicode
'\n'
'\012'    // '\n' in ottale
'\t'

Le sequenze di escape valide sono elencate nella tabella 2.3.

Tabella 2.3. Sequenze di escape per i caratteri.

SequenzaUnicodeSignificato

\b

\u0008

backspace

\t

\u0009

tabulazione orizzontale

\n

\u000a

fine riga

\f

\u000c

salto pagina

\r

\u000d

ritorno a capo

\"

\u0022

virgolette (")

\'

\u0027

apice (')

\\

\u0009

barra inversa (\)

Letterali stringa

Un letterale stringa è una sequenza di caratteri racchiusa tra virgolette o tra triple di virgolette, cioè """…""".

Per i letterali stringa tra virgolette, i caratteri permessi sono gli stessi dei letterali carattere. Tuttavia, se nella stringa compare un carattere di virgoletta ", ne deve essere effettuato l’escape con un carattere \. Ecco alcuni esempi.

"Programmare\nin\nScala"
"Egli esclamò: \"Scala è fantastico!\""
"Primo\tSecondo"

I letterali stringa racchiusi tra triple di virgolette sono anche chiamati letterali stringa multiriga. Queste stringhe possono estendersi su più righe; i caratteri di fine riga faranno parte della stringa. Possono includere qualsiasi carattere, compresi apici e virgolette, ma non tre virgolette di seguito. Sono utili se si vogliono inserire caratteri \ che non formano sequenze Unicode o di escape valide come quelle elencate nella tabella 2.3. Le espressioni regolari sono un tipico esempio, che discuteremo nel capitolo 3. Comunque, le sequenze di escape che compaiono non vengono interpretate.

Ecco tre stringhe di esempio.

"""Programmare\nin\nScala"""
"""Egli esclamò: "Scala è fantastico!" """
"""Prima riga\n
Seconda riga\t

Quarta riga"""

Notate che abbiamo dovuto aggiungere uno spazio prima delle """ conclusive nel secondo esempio per evitare un errore di sintassi. Provare a effettuare l’escape della seconda " che termina la citazione "Scala è fantastico!", cioè "Scala è fantastico!\", non funziona.

Copiate queste stringhe e incollatele nell’interprete scala. Fate la stessa cosa per le stringhe di esempio precedenti. Quali sono le differenze di interpretazione?

Letterali simbolo

Scala supporta i simboli, che sono stringhe internate nel senso che due simboli con lo stesso “nome”, cioè con la stessa sequenza di caratteri, fanno riferimento allo stesso oggetto in memoria. I simboli sono usati meno spesso in Scala rispetto ad altri linguaggi come Ruby, Smalltalk e Lisp. Sono utili come chiavi di mappe al posto delle stringhe.

Un letterale simbolo è costituito da un apice ', seguito da una lettera, seguita da zero o più cifre e lettere. Notate che un’espressione come '1 non è valida, perché il compilatore pensa che sia un letterale carattere incompleto.

Un letterale simbolo 'id è un’abbreviazione per l’espressione scala.Symbol("id").

Se volete creare un simbolo che contiene spazi bianchi, usate per esempio scala.Symbol(" Programmare in Scala "). Tutto lo spazio bianco viene mantenuto.

Tuple

Quante volte avreste voluto restituire due o più valori da un metodo? In molti linguaggi, come in Java, avete solo alcune opzioni per farlo, nessuna delle quali è particolarmente attraente. Potreste passare al metodo alcuni parametri che verranno modificati per tutti o per alcuni valori “di ritorno”, ma questa è una soluzione dozzinale. Oppure potreste dichiarare qualche piccola classe “strutturale” che contiene quei valori e poi restituire un’istanza di quella classe.

Scala supporta le tuple, cioè raggruppamenti di due o più valori, di solito create con una sintassi letterale che rappresenta una lista di oggetti separati da virgole racchiusa tra parentesi, per esempio (x1, x2, …). I tipi degli elementi xi non sono correlati tra loro, quindi potete mescolarli. Questi “raggruppamenti” letterali vengono creati come istanze di scala.TupleN, dove N è il numero di elementi nella tupla. La API di Scala definisce classi TupleN separate per N che va da 1 a 22 (estremi inclusi). Le tuple sono valori di prima classe immutabili, quindi potete assegnarle a variabili, passarle come parametri e restituirle dai metodi.

L’esempio seguente mostra l’uso delle tuple.

// esempi/cap-2/tuple-example-script.scala

def tupleator(x1: Any, x2: Any, x3: Any) = (x1, x2, x3)

val t = tupleator("Ciao", 1, 2.3)
println("Stampa l'intera tupla:      " + t)
println("Stampa il primo elemento:   " + t._1)
println("Stampa il secondo elemento: " + t._2)
println("Stampa il terzo elemento:   " + t._3)

val (t1, t2, t3) = tupleator("Mondo", '!', 0x22)
println(t1 + " " + t2 + " " + t3)

L’esecuzione di questo script con scala produce l’uscita seguente.

Stampa l'intera tupla:      (Ciao,1,2.3)
Stampa il primo elemento:   Ciao
Stampa il secondo elemento: 1
Stampa il terzo elemento:   2.3
Mondo ! 34

Il metodo tupleator restituisce semplicemente una “3-tupla” con gli argomenti in ingresso. La prima istruzione che usa questo metodo assegna la tupla restituita a una singola variabile t. Le quattro istruzioni successive stampano t in diversi modi. Il primo comando di stampa invoca Tuple3.toString, che racchiude la lista degli elementi tra parentesi. I tre comandi successivi stampano ogni elemento di t separatamente. L’espressione t._N recupera l’elemento di posto N, cominciando da 1, non da 0 (questa scelta segue le convenzioni della programmazione funzionale).

Le ultime due righe mostrano come usare un’espressione tupla sul lato sinistro di un assegnamento. Dichiariamo tre variabili di tipo val (t1, t2 e t3) per contenere i singoli elementi della tupla. In sostanza, gli elementi della tupla vengono estratti automaticamente.

Notate come abbiamo mescolato tipi diversi nella tupla. Potete vedere i tipi più chiaramente se usate la modalità interattiva del comando scala che abbiamo introdotto nel capitolo 1.

Invocate il comando scala senza passare nessuno script come argomento. Al prompt scala> digitate val t = ("Ciao", 1, 2.3) e verificate di ottenere il seguente risultato che vi mostra il tipo di ogni elemento nella tupla.

scala> val t = ("Ciao", 1, 2.3)
t: (java.lang.String, Int, Double) = (Ciao, 1, 2.3)

Vale la pena di notare che c’è più di un modo per definire una tupla. Finora abbiamo usato la sintassi parentetica più comune, ma potete anche usare l’operatore freccia tra due valori, così come alcuni metodi di costruzione speciali delle classi relative alle tuple.

scala> 1 -> 2
res0: (Int, Int) = (1,2)

scala> Tuple2(1, 2)
res1: (Int, Int) = (1,2)

scala> Pair(1, 2)
res2: (Int, Int) = (1,2)

Option, Some e None: evitare i valori nulli

Parleremo della gerarchia standard dei tipi di Scala nella sezione La gerarchia di tipi di Scala del capitolo 7. Tuttavia, ci sono tre classi utili da capire subito: la classe Option e le sue due sottoclassi, Some e None.

La maggior parte dei linguaggi usa una parola chiave o un oggetto particolare per assegnare un valore alle variabili quando non c’è nient’altro a cui possono fare riferimento. Java usa null, Ruby usa nil. In Java null è una parola chiave, non un oggetto, e quindi è illegale invocare un qualsiasi metodo su di essa. Ma questa scelta da parte del progettista del linguaggio genera confusione: perché restituire una parola chiave quando il programmatore si aspetta un oggetto?

Per essere più coerenti con lo scopo di rendere ogni cosa un oggetto e per seguire le convenzioni della programmazione funzionale, Scala vi incoraggia a usare il tipo Option per le variabili e per i valori di ritorno delle funzioni che potrebbero o meno fare riferimento a un valore. Quando non c’è alcun valore usate None, un object che è una sottoclasse di Option. Quando c’è un valore usate Some, che racchiude il valore; anche Some è una sottoclasse di Option.

None è dichiarato come un object, non come una classe, perché in realtà ce ne serve una sola istanza. In questo senso è come la parola chiave null, ma è un oggetto reale con i propri metodi.

Potete vedere Option, Some e None in azione nell’esempio seguente, dove viene creata una mappa delle capitali di stato americane.

// esempi/cap-2/state-capitals-subset-script.scala

val stateCapitals = Map(
  "Alabama" -> "Montgomery",
  "Alaska"  -> "Juneau",
  // ...
  "Wyoming" -> "Cheyenne")

println("Recupera le capitali in istanze di Option:")
println("Alabama: " + stateCapitals.get("Alabama"))
println("Wyoming: " + stateCapitals.get("Wyoming"))
println("Ignoto:  " + stateCapitals.get("Ignoto"))

println("Recupera le capitali dalle istanze di Option:")
println("Alabama: " + stateCapitals.get("Alabama").get)
println("Wyoming: " + stateCapitals.get("Wyoming").getOrElse("Oops!"))
println("Ignoto:  " + stateCapitals.get("Ignoto").getOrElse("Oops2!"))

La conveniente sintassi -> per definire coppie nome-valore e inizializzare una Map verrà discussa nella sezione L’oggetto Predef del capitolo 7. Per ora, vogliamo concentrarci sui due gruppi di istruzioni println, che usiamo per mostrare cosa succede quando recuperate i valori dalla mappa. Se eseguite questo script con il comando scala, otterrete l’uscita seguente.

Recupera le capitali in istanze di Option:
Alabama: Some(Montgomery)
Wyoming: Some(Cheyenne)
Ignoto:  None
Recupera le capitali dalle istanze di Option:
Alabama: Montgomery
Wyoming: Cheyenne
Ignoto:  Oops2!

Il primo gruppo di istruzioni println invoca implicitamente toString sulle istanze restituite da get. Stiamo chiamando toString su istanze di Some o di None, perché i valori restituiti da Map.get vengono automaticamente incorporati in un’istanza di Some quando la mappa contiene un valore corrispondente alla chiave specificata. Notate che la libreria Scala non memorizza l’istanza di Some nella mappa, ma vi incorpora il valore nel momento in cui viene recuperato. Invece, quando chiediamo alla mappa una voce che non esiste, viene restituito l’oggetto None al posto di null. Questo accade nell’ultima delle prime tre istruzioni println.

Il secondo gruppo di istruzioni println compie un passo ulteriore. Dopo aver chiamato Map.get, esse invocano get oppure getOrElse su ogni istanza di Option per recuperare il valore che contiene. Option.get richiede che l’istanza non sia vuota, cioè che l’istanza di Option sia in realtà di tipo Some. In questo caso, get restituisce il valore racchiuso nell’istanza di Some, come mostra l’istruzione println che stampa la capitale dell’Alabama. Tuttavia, se l’istanza di Option in realtà è di tipo None, allora None.get lancia una eccezione NoSuchElementException.

Nelle ultime due istruzioni println mostriamo anche il metodo alternativo getOrElse. Questo metodo restituisce il valore racchiuso in Option se l’istanza di Option è di tipo Some, oppure restituisce l’argomento passato a getOrElse se l’istanza è di tipo None. In altre parole, l’argomento di getOrElse serve come valore di ritorno predefinito.

Quindi getOrElse è il più difensivo dei due metodi, poiché evita l’eventualità di lanciare una eccezione. Discuteremo i meriti di alternative come quella tra get e getOrElse nella sezione Le eccezioni e le alternative nel capitolo 13.

Notate che, restituendo un’istanza di Option, Map.get segnala in modo automatico la possibilità che non ci sia alcun elemento corrispondente alla chiave specificata. La mappa gestisce questa situazione restituendo None. La maggior parte dei linguaggi restituirebbe null (o il suo equivalente) quando non c’è nessun valore “reale” da restituire; si impara dall’esperienza ad aspettarsi un possibile null. Usare Option rende il comportamento più esplicito nella firma del metodo, quindi più autodescrittivo.

In più, grazie alla tipizzazione statica di Scala, non potete fare l’errore di “dimenticare” che viene restituita un’istanza di Option e tentare di invocare un metodo supportato dal tipo del valore racchiuso nell’istanza (se c’è un valore). In Java, quando un metodo restituisce un valore, è facile dimenticarsi di controllare se è null prima di invocare un metodo su di esso. Quando un metodo Scala restituisce Option, il controllo di tipo effettuato dal compilatore vi obbliga a estrarre il valore dall’istanza di Option prima di invocare un metodo su di esso. Questo vi “ricorda” di controllare se l’istanza di Option è in effetti un’istanza di None. Quindi, l’uso di Option vi incoraggia decisamente a programmare in maniera più resiliente.

Dato che Scala funziona sulla JVM e su .NET e deve interoperare con altre librerie, ha bisogno di supportare null. Tuttavia, dovreste evitare di usare null nel vostro codice. Tony Hoare, che ha inventato il riferimento nullo nel 1965 mentre lavorava su un linguaggio orientato agli oggetti chiamato ALGOL W, ha chiamato la sua invenzione “l’errore da un miliardo di dollari” [Hoare2009]. Evitate di contribuire a quella cifra.

Quindi, come scrivereste un metodo che restituisce Option? Ecco una ragionevole implementazione di get che potrebbe essere usata in una sottoclasse concreta di Map (l’originale Map.get è astratto). Per una versione più sofisticata, si veda l’implementazione di get nella classe scala.collection.immutable.HashMap contenuta nella distribuzione del codice sorgente della libreria Scala.

def get(key: A): Option[B] = {
  if (contains(key))
    new Some(getValue(key))
  else
    None
}

Map definisce anche il metodo contains, che restituisce true se la mappa contiene un valore per la chiave specificata. Il metodo getValue va considerato come un metodo interno deputato a recuperare il valore dalla struttura sottostante che gestisce la memorizzazione, qualunque essa sia.

Notate come il valore restituito da getValue venga racchiuso in un’istanza di Some[B], dove il tipo B viene inferito. Tuttavia, se l’invocazione di contains(key) restituisce false, allora viene restituito l’oggetto None.

Potete usare questo stesso idioma quando i vostri metodi restituiscono Option. Esploreremo altri usi di Option nelle sezioni seguenti. Il suo uso pervasivo in Scala lo rende un concetto importante da capire.

Organizzare il codice in file e spazi di nomi

Scala adotta il concetto di package usato da Java per gli spazi di nomi, ma offre una sintassi più flessibile. Proprio come i nomi di file non devono corrispondere ai nomi di tipo, la struttura dei package non deve corrispondere alla struttura delle directory. Quindi potete definire i package nei file a prescindere dalla loro ubicazione “fisica”.

L’esempio seguente definisce una classe MyClass in un package com.example.mypkg usando la convenzionale sintassi Java.

// esempi/cap-2/package-example1.scala

package com.example.mypkg

class MyClass {
  // ...
}

Il prossimo è un esempio artificioso che definisce alcuni package usando la sintassi per i package annidati in Scala, che è simile alla sintassi per gli spazi di nomi in C# e all’uso dei moduli come spazi di nomi in Ruby.

// esempi/cap-2/package-example2.scala

package com {
  package example {
    package pkg1 {
      class Class11 {
        def m = "m11"
      }
      class Class12 {
        def m = "m12"
      }
    }

    package pkg2 {
      class Class21 {
        def m = "m21"
        def makeClass11 = {
          new pkg1.Class11
        }
        def makeClass12 = {
          new pkg1.Class12
        }
      }
    }

    package pkg3.pkg31.pkg311 {
      class Class311 {
        def m = "m21"
      }
    }
  }
}

Due package pkg1 e pkg2 sono definiti all’interno del package com.example. Un totale di tre classi è definito nei due package. I metodi makeClass11 e makeClass12 nella classe Class21 mostrano come fare riferimento a un tipo contenuto in un package “fratello”, in questo caso pkg1. Potete anche fare riferimento a queste classi con il loro percorso completo, com.example.pkg1.Class11 e com.example.pkg1.Class12 rispettivamente.

Il package pkg3.pkg31.pkg311 mostra che potete “concatenare” insieme diversi package in una sola clausola. Non è necessario usare clausole package separate per ogni package.

Seguendo le convenzioni di Java, il package radice per le classi della libreria Scala è chiamato scala.

Scala non consente di usare dichiarazioni di package negli script che vengono eseguiti direttamente con l’interprete scala. La ragione ha a che fare con il modo in cui l’interprete converte le istruzioni contenute negli script in codice Scala valido prima di compilarlo in bytecode. Si veda la sezione Lo strumento scala a riga di comando nel capitolo 14 per maggiori dettagli.

Importare i tipi e i loro membri

Per usare le dichiarazioni nei package è necessario importarle, esattamente come in Java e similmente ad altri linguaggi. Tuttavia, rispetto a Java, Scala incrementa notevolmente il numero delle vostre opzioni. Gli esempi seguenti illustrano diversi modi di importare i tipi di Java.

// esempi/cap-2/import-example1.scala

import java.awt._
import java.io.File
import java.io.File._
import java.util.{Map, HashMap}

Potete importare tutti i tipi contenuti in un package usando il trattino basso _ in qualità di wildcard, come mostrato nella prima riga. Potete anche importare singoli tipi Scala o Java, come mostrato nella seconda riga.

Java usa il carattere di asterisco * come wildcard che corrisponde a tutti i tipi contenuti in un package, o a tutti i membri statici appartenenti a un tipo quando effettua una “importazione statica”. In Scala è possibile usare questo carattere nel nome dei metodi, quindi viene usato _ in qualità di wildcard, come abbiamo visto in precedenza.

Come mostrato nella terza riga, potete importare tutti i metodi e i campi statici appartenenti a un tipo Java. Se java.io.File fosse effettivamente un object Scala, come discusso in precedenza, allora questa riga importerebbe i campi e i metodi dell’oggetto.

Infine, potete importare selettivamente solo i tipi che vi interessano. Nella quarta riga importiamo solo i tipi java.util.Map e java.util.HashMap dal package java.util. Confrontate questa istruzione di importazione su una sola riga con le istruzioni di importazione su due righe che abbiamo usato nel nostro primo esempio nella sezione Inferire le informazioni di tipo. I due casi sono funzionalmente equivalenti.

L’esempio successivo mostra alcune opzioni più avanzate per le istruzioni di importazione.

// esempi/cap-2/import-example2-script.scala

def writeAboutBigInteger() = {

  import java.math.BigInteger.{
    ONE => _,
    TEN,
    ZERO => JAVAZERO }

  // ONE è effettivamente indefinito
  // println("ONE: " + ONE)
  println("TEN: " + TEN)
  println("ZERO: " + JAVAZERO)
}

writeAboutBigInteger()

Questo esempio mostra due caratteristiche. Primo, possiamo collocare le istruzioni di importazione quasi ovunque, non solo in cima al file come richiesto da Java. Questa caratteristica ci consente di restringere l’ambito di visibilità delle importazioni. Per esempio, non possiamo fare riferimento alle definizioni importate da BigInteger al di fuori dell’ambito del metodo. Un altro vantaggio di questa caratteristica è la possibilità di collocare un’istruzione di importazione più vicino al punto in cui gli elementi importati vengono effettivamente usati.

La seconda caratteristica mostrata è l’abilità di rinominare gli elementi importati. Qui, alla costante java.math.BigInteger.ONE viene assegnato come nome la wildcard del trattino basso. Questo la rende effettivamente invisibile e indisponibile nell’ambito di importazione. Questa tecnica è utile quando volete importare ogni cosa tranne alcuni elementi particolari.

Poi, la costante java.math.BigInteger.TEN viene importata senza essere rinominata, in modo da potervi fare riferimento semplicemente come TEN.

Infine, alla costante java.math.BigInteger.ZERO viene dato l’alias JAVAZERO.

Gli alias sono utili se volete dare un nome più conveniente all’elemento o se volete evitare ambiguità con altri elementi che hanno lo stesso nome nello stesso ambito.

Le importazioni sono relative

C’è un’altra cosa importante da sapere sulle importazioni: sono relative. Notate i commenti per le importazioni che seguono.

// esempi/cap-2/relative-imports.scala

import scala.collection.mutable._
import collection.immutable._         // Dato che "scala" è già importato
import _root_.scala.collection.jcl._  // Percorso completo dalla "radice" reale

package scala.actors {
  import remote._                     // Siamo nell'ambito di "scala.actors"
}

Notate che l’ultima istruzione di importazione annidata nell’ambito del package scala.actor è relativa a quell’ambito.

Il wiki di Scala [ScalaWiki] contiene altri esempi all’indirizzo http://scala.sygneca.com/faqs/language#how-do-i-import.

È piuttosto raro incontrare difficoltà con le importazioni relative, ma il problema di questa convenzione è che talvolta causa sorprese, specialmente se siete abituati a linguaggi come Java dove le importazioni sono assolute. Se il compilatore vi restituisce un errore ingannevole informandovi di non aver trovato un package, controllate che le istruzioni di importazione siano opportunamente relative o aggiungete il prefisso _root_.. In più, se vedete che il vostro IDE o un altro strumento inserisce l’istruzione import _root_… nel vostro codice, ora sapete cosa significa.

Ricordate che le istruzioni di importazione sono relative, non assolute. Per creare un percorso assoluto, fatelo cominciare con _root_.

Tipi astratti e tipi parametrici

Nella sezione Un assaggio di Scala del capitolo 1 abbiamo accennato al supporto di Scala per i tipi parametrici, che sono molto simili ai generici in Java. (I due termini sono intercambiabili, ma è più comune usare “tipi parametrici” nella comunità Scala e “generici” nella comunità Java.) La differenza più ovvia è nella sintassi, dove Scala usa le parentesi quadre ([…]) mentre Java usa le parentesi angolari (<…>).

Per esempio, una lista di stringhe verrebbe dichiarata nel modo seguente.

val languages: List[String] = …

Ci sono altre differenze importanti rispetto ai generici di Java che esploreremo nella sezione Capire i tipi parametrici del capitolo 12.

Per ora, menzioneremo un’altra caratteristica utile che incontrerete prima di arrivare al capitolo 12, dove viene spiegata nei dettagli. Se osservate la dichiarazione di scala.List nella documentazione Scaladoc, noteterete che la dichiarazione è scritta come … List[+A]. Il segno “+” davanti ad A significa che List[B] è un sottotipo di List[A] per qualsiasi B che sia sottotipo di A. Se trovate un “-” davanti al tipo parametrico, allora la relazione va in senso opposto: Foo[B] è un supertipo di Foo[A] se la dichiarazione è Foo[-A].

Scala supporta un altro meccanismo di astrazione di tipo chiamato tipi astratti, usato in molti linguaggi funzionali come Haskell. I tipi astratti vennero anche presi in considerazione per essere inclusi in Java quando furono adottati i generici. Vogliamo presentarli ora perché ne vedrete molti esempi prima di immergervi nei loro dettagli nel capitolo 12. Per un confronto molto dettagliato tra questi due meccanismi, si veda [Bruce1998].

I tipi astratti possono essere applicati a molti degli stessi problemi di progettazione per i quali vengono impiegati i tipi parametrici. Tuttavia, sebbene i due meccanismi si sovrappongano, non sono ridondanti. Ognuno ha i propri punti di forza e di debolezza per certi problemi di progettazione.

Ecco un esempio che usa un tipo astratto.

// esempi/cap-2/abstract-types-script.scala

import java.io._

abstract class BulkReader {
  type In
  val source: In
  def read: String
}

class StringBulkReader(val source: String) extends BulkReader {
  type In = String
  def read = source
}

class FileBulkReader(val source: File) extends BulkReader {
  type In = File
  def read = {
    val in = new BufferedInputStream(new FileInputStream(source))
    val numBytes = in.available()
    val bytes = new Array[Byte](numBytes)
    in.read(bytes, 0, numBytes)
    new String(bytes)
  }
}

println(new StringBulkReader("Ciao Scala!").read )
println(new FileBulkReader(new File("abstract-types-script.scala")).read)

L’esecuzione di questo script con scala produce l’uscita seguente.

Ciao Scala!
import java.io._

abstract class BulkReader {
…

La classe astratta BulkReader dichiara tre membri astratti, un type chiamato In, un campo val chiamato source e un metodo read. Come in Java, anche in Scala le istanze possono essere create solo a partire da classi concrete, in cui tutti i membri devono avere una definizione.

Le classi derivate, StringBulkReader e FileBulkReader, forniscono definizioni concrete per questi membri astratti. Tratteremo i dettagli delle dichiarazioni di classe nel capitolo 5 e i particolari relativi alla ridefinizione delle dichiarazioni dei membri nella sezione Ridefinire i membri di classi e tratti del capitolo 6.

Per ora, notate che il campo type funziona in modo molto simile a un parametro di tipo in un tipo parametrico. In effetti, potremmo riscrivere questo esempio come segue, mostrando solo ciò che sarebbe differente.

abstract class BulkReader[In] {
  val source: In
  …
}

class StringBulkReader(val source: String) extends BulkReader[String] {…}

class FileBulkReader(val source: File) extends BulkReader[File] {…}

Esattamente come per i tipi parametrici, se definiamo il tipo In come String allora anche il campo source deve essere definito come String. Notate che il metodo read di StringBulkReader restituisce semplicemente il campo source, mentre il metodo read di FileBulkReader legge i contenuti del file.

Come mostrato in [Bruce1998], i tipi parametrici tendono a essere la scelta migliore per le collezioni, che è il modo in cui sono usati più spesso nel codice Java, mentre i tipi astratti sono più utili per “famiglie” di tipi e per altri scenari.

Esploreremo i dettagli dei tipi astratti di Scala nel capitolo 12. Per esempio, vedremo come vincolare i tipi concreti accettabili che possono essere usati.

Parole riservate

La tabella 2.4 elenca le parole riservate in Scala, a volte chiamate anche “parole chiave”, e descrive brevemente come vengono usate [ScalaSpec2009].

Tabella 2.4. Parole riservate.

ParolaDescrizioneSi veda…

abstract

Rende astratta una dichiarazione. A differenza di Java, la parola chiave di solito non è obbligatoria per i membri astratti.

la sezione I concetti di base per classi e oggetti nel capitolo 5

case

Comincia una clausola case in una espressione di pattern matching.

la sezione Pattern matching nel capitolo 3

catch

Comincia una clausola per catturare le eccezioni lanciate.

la sezione Usare le clausole try, catch e finally nel capitolo 3

class

Comincia una dichiarazione di classe.

la sezione I concetti di base per classi e oggetti nel capitolo 5

def

Comincia una dichiarazione di metodo.

la sezione Dichiarazioni di metodo in questo capitolo

do

Comincia un ciclo do … while.

la sezione Altri costrutti di ciclo nel capitolo 3

else

Comincia una clausola else per una clausola if.

la sezione Le istruzioni if in Scala nel capitolo 3

extends

Indica che la classe o il tratto che segue è il tipo genitore della classe o del tratto che viene dichiarato.

la sezione Classi genitore nel capitolo 5

false

Falsità logica.

la sezione La gerarchia di tipi di Scala nel capitolo 7

final

Applicata a una classe o a un tratto, ne proibisce la derivazione di tipi figlio. Applicata a un membro, ne proibisce la ridefinizione in una classe o in un tratto derivato.

la sezione Tentare di ridefinire le dichiarazioni final nel capitolo 6

finally

Comincia una clausola eseguita dopo la corrispondente clausola try, anche se la clausola try lancia una eccezione.

la sezione Usare le clausole try, catch e finally nel capitolo 3

for

Comincia una espressione for (ciclo).

la sezione Le espressioni for in Scala nel capitolo 3

forSome

Usata nelle dichiarazioni di tipi esistenziali per vincolare i tipi concreti consentiti che possono essere usati.

la sezione Tipi esistenziali nel capitolo 12

if

Comincia una clausola if.

la sezione Le istruzioni if in Scala nel capitolo 3

implicit

Segnala un metodo come un convertitore di tipo implicito. Segnala un parametro di un metodo come opzionale, purché un oggetto sostitutivo di tipo compatibile sia visibile nell’ambito in cui il metodo viene invocato.

la sezione Conversioni implicite nel capitolo 8

import

Importa uno o più tipi o membri di un tipo nell’ambito corrente.

la sezione Importare i tipi e i loro membri in questo capitolo

lazy

Posticipa la valutazione di una variabile di tipo val.

la sezione Valori ritardati nel capitolo 8

match

Comincia una clausola di pattern matching.

la sezione Pattern matching nel capitolo 3

new

Crea una nuova istanza di una classe.

la sezione I concetti di base per classi e oggetti nel capitolo 5

null

Il valore di una variabile a cui non è stato assegnato nessun valore.

la sezione La gerarchia di tipi di Scala nel capitolo 7

object

Comincia una dichiarazione di singleton, cioè una classe con una sola istanza.

la sezione Classi e oggetti: dove sono i membri statici? nel capitolo 7

override

Ridefinisce un membro concreto di una classe o di un tratto, purché l’originale non sia qualificato come final.

la sezione Ridefinire i membri di classi e tratti nel capitolo 6

package

Comincia una dichiarazione di package.

la sezione Organizzare il codice in file e spazi di nomi in questo capitolo

private

Restringe la visibilità di una dichiarazione.

la sezione Regole di visibilità nel capitolo 5

protected

Restringe la visibilità di una dichiarazione.

la sezione Regole di visibilità nel capitolo 5

requires

Deprecata. Veniva usata per la tipizzazione della classe corrente (self typing).

la sezione La gerarchia di tipi di Scala nel capitolo 7

return

Ritorna da una funzione.

la sezione Un assaggio di Scala nel capitolo 1

sealed

Appicata a una classe genitore per obbligare tutte le classi derivate a venire dichiarate nello stesso file sorgente.

la sezione Classi case nel capitolo 6

super

Analogo a this, ma usato per fare riferimento al tipo genitore.

la sezione Ridefinire i metodi astratti e concreti nel capitolo 6

this

Il modo in cui un oggetto fa riferimento a se stesso. Il nome del metodo per i costruttori ausiliari.

la sezione I concetti di base per classi e oggetti nel capitolo 5

throw

Lancia una eccezione.

la sezione Usare le clausole try, catch e finally nel capitolo 3

trait

Un modulo mixin che aggiunge stato e comportamento a una istanza di una classe.

il capitolo 4

try

Comincia un blocco che potrebbe lanciare una eccezione.

la sezione Usare le clausole try, catch e finally nel capitolo 3

true

Verità logica.

la sezione La gerarchia di tipi di Scala nel capitolo 7

type

Comincia una dichiarazione di tipo.

la sezione Tipi astratti e tipi parametrici in questo capitolo

val

Comincia una dichiarazione di “variabile” a sola lettura.

la sezione Dichiarazioni di variabile in questo capitolo

var

Comincia una dichiarazione di variabile a lettura/scrittura.

la sezione Dichiarazioni di variabile in questo capitolo

while

Comincia un ciclo while.

la sezione Altri costrutti di ciclo nel capitolo 3

with

Include il tratto che segue nella classe che viene dichiarata o nell’oggetto che viene istanziato.

il capitolo 4

yield

Restituisce un elemento in una espressione for che diventa parte di una sequenza.

la sezione Produrre gli elementi in uscita nel capitolo 3

_

Un segnaposto usato nelle importazioni, nei letterali funzione, &c.

In molti punti

:

Separatore tra identificatori e annotazioni di tipo.

la sezione Un assaggio di Scala nel capitolo 1

=

Assegnamento.

la sezione Un assaggio di Scala nel capitolo 1

=>

Usato nei letterali funzione per separare la lista di argomenti dal corpo della funzione.

la sezione Letterali funzione e chiusure nel capitolo 8

<-

Usato nelle espressioni for nelle espressioni generatore.

la sezione Le espressioni for in Scala nel capitolo 3

<:

Usato nelle dichiarazioni di tipi parametrici e astratti per vincolare i tipi permessi.

la sezione Limiti sui tipi nel capitolo 12

<%

Usato nelle dichiarazioni per i “limiti di vista” nelle dichiarazioni di tipi parametrici e astratti.

la sezione Viste e limiti sulle viste nel capitolo 12

>:

Usato nelle dichiarazioni di tipi parametrici e astratti per vincolare i tipi permessi.

la sezione Limiti sui tipi nel capitolo 12

#

Usato nelle proiezioni di tipo.

la sezione Proiezioni di tipo nel capitolo 12

@

Segnala una annotazione.

la sezione Annotazioni nel capitolo 13

(Unicode \u21D2) uguale a =>.

la sezione Letterali funzione e chiusure nel capitolo 8

(Unicode \u2190) uguale a <-.

la sezione Le espressioni for in Scala nel capitolo 3

Notate che break e continue non sono elencate. Queste parole di controllo non esistono in Scala. Invece, Scala vi incoraggia a usare gli idiomi della programmazione funzionale che di solito sono più concisi e meno soggetti a errori. Discuteremo alcuni approcci alternativi parlando dei cicli for (si veda la sezione Espressioni generatore nel capitolo 3).

Alcuni metodi Java usano nomi che sono riservati in Scala, per esempio java.util.Scanner.match. Per evitare errori di compilazione, circondate il nome con caratteri di apice inverso, come in java.util.Scanner.‵match‵.

Riepilogo, e poi?

Abbiamo descritto i vari modi in cui la sintassi di Scala è concisa, flessibile e produttiva. Abbiamo anche descritto diverse caratteristiche di Scala. Nel prossimo capitolo completeremo la trattazione degli elementi essenziali del linguaggio prima di immergerci nel supporto di Scala per la programmazione orientata agli oggetti e per la programmazione funzionale.

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