Introduzione a Scala

I parte: I perche‘ di un linguaggiodi

Negli ultimi anni abbiamo assistito al proliferare di linguaggi JVM, alcuni come risultato di porting, altri invece affermatisi come vere e proprie innovazioni. Scala è tra questi, avendo un paradigma nuovo, ibrido (funzionale e orientato agli oggetti) totalmente integrabile in progetti Java ma anche ideale per definire Domain Specific Language (DSL). In questo articolo vedremo quali sono i motivi a nostro avviso più importanti per cui Scala dovrebbe essere adottato. Nel far ciò, introdurremo i concetti di base di sintassi di questo linguaggio, quelli che più lo differenziano da Java, che verranno approfonditi nei prossimi articoli.

Perche' Scala?

Scala [1] è un linguaggio ibrido, funzionale e orientato agli oggetti, creato da Martin Odersky [2], già autore dell'attuale compilatore javac e uno dei padri dei generics di Java. La prima versione venne rilasciata nel 2003, dopo due anni di design. L'ultima versione stabile è la 2.9.0, uscita a metà maggio di quest'anno.

L'ambiente può essere scaricato dal sito ufficiale [1] e comprende il compilatore, scalac, che prende una riga di comando e fornisce degli output (.class, .jar) analoghi a javac. Il codice che daremo a titolo d'esempio in questa serie di articoli può essere lanciato dall'interprete tramite comando scala.

Ci sono molti motivi per cui un programmatore Java, a nostro avviso, dovrebbe imparare Scala e usarlo: vediamone i più importanti.

Compatibilità

Innanzitutto Scala è open source e compatibile; infatti, una volta compilato con il suo compilatore, il codice diventa normalissimo bytecode per la JVM che può usare librerie Java ed essere perfettamente integrato in applicazioni Java esistenti, senza referenziare classloader particolari perche' non viene interpretato a run-time, come Groovy o Clojure.

Scalabilità

Le applicazioni moderne necessitano sempre più di nascere scalabili per tanti motivi; Scala (SCAlable LAnguage) permette e anzi favorisce la scrittura di un codice che come può essere eseguito in un'applicazione console di un PC, può essere eseguito in un cluster. Come sappiamo, in Java non è semplice rendere un'applicazione o una libreria veramente scalabili: ciò richiede uno sforzo di design non indifferente e il risultato può essere un codice sì scalabile ma spesso di difficile gestione, lettura e manutenzione.

Leggibilità ed eleganza

Il terzo motivo è infatti è che Scala è conciso e di alto livello. Ciò permette non solo di realizzare un DSL in modo molto semplice, ma anche di risolvere alcuni problemi tipici della maggioranza dei linguaggi.

Qualche punto a favore di Scala

A questo proposito è opportuna una digressione su quelle che potremmo chiamare tipiche "frustrazioni" che uno sviluppatore Java prova nel suo lavoro quotidiano. A nostro avviso la più importante riguarda le Collection: in Java, lavorare con le collezioni porta a codice verboso, spesso poco scalabile e contro-intuitivo, se paragonato a Scala.

Concisione

Vediamo un confronto: supponiamo di volere un insieme dei quadrati dei numeri dispari da 1 a 19.

In Java, lo scriveremmo così:

LinkedList data = new LinkedList();
for( int i=1  ;  i <= 19  ;  i += 2 )
       input.add(Math.pow(i, 2));

In Scala:

val data = for( i

 

Chiaramente, questo è più conciso, elegante e leggibile: esso ci dice che data è un elenco di numeri che si ottiene filtrando i numeri dispari dagli interi da 1 a 19 elevati alla seconda.

Immaginiamo che ad un certo punto nel programma ci serva non più solo lista ma anche una mappa da uno di questi valori ad una stringa presente in un'altra lista, all'indice corrispondente. In Java ci concentreremmo su come costruirla:

List data2 = ... // lista delle stringhe
Map<Double, String> mappedData = new HashMap<Double, String>();
for(int i=0 ; i<input.size(); ++i) {
   mappedSquare.put(data.get(i), data2.get(i));
}

In Scala, invece, ci focalizzeremmo più su cosa realizzare:

val mappedData = (data zip data2) toMap

Ossia vogliamo esattamente una mappa creata dall'unione di data con data2. Il primo blocco sarebbe ancora più "parlante", se fosse scritto così:

val dispari = ((_ : Int) % 2 == 1)
 
def allaSeconda(base :Int) = pow(base, 2)
 
val mySquares = (( 1 to 19 ) filter dispari) map allaSeconda

D'ora in poi chiameremo questo blocco di codice Listato 1. Potremmo vederlo come un germe di un nostro DSL per la nostra libreria: esso dapprima definisce la funzione per discriminare i numeri dispari e la funzione per elevare alla seconda potenza quindi usa queste funzioni in funzioni d'ordine superiore, ossia funzioni che prendono altre funzioni come parametri.

Anche se questi esempi sono molto banali (possiamo comunque facilmente immaginare di sostituire agli interi e alle stringhe oggetti più complessi), troviamo in essi molti aspetti peculiari di Scala. Il primo è che non abbiamo bisogno di separare le istruzioni con il punto e virgola, il compilatore nella stragrande maggioranza dei casi è in grado di capire da solo quando finisce un'istruzione; ciò non toglie che possiamo comunque inserire il punto e virgola, oppure un doppio capo riga, se serve a noi o se abbiamo concatenato le espressioni in modo tale che per qualche ambiguità il compilatore non può separare le istruzioni.

Immutabilità

In secondo luogo osserviamo la keyword val che indica che la variabile lì definita è immutabile, cioè non è riassegnabile. Se si prova a riassegnarla si ottiene un errore di compilazione:

val a = 10
a = a + 1 // errore: reassignment to val

L'immutabilità è un aspetto tipico dei linguaggi funzionali e i creatori di Scala favoriscono fortemente l'utilizzo di variabili e oggetti immutabili (quindi anche collezioni immutabili) perche' consentono una maggiore scalabilità e sicurezza, sollevando inoltre dalla necessità di lock e altri meccanismi di sincronismo quando si utilizza il codice in parallelo. Infatti utilizzare oggetti immutabili significa generalmente produrre codice predicibile: per il compilatore, per eventuali strumenti di ottimizzazione o parallelizzazione, e per lo sviluppatore; in definitiva, si realizza dunque del codice più leggibile.

Può sembrare dispendioso, dal punto di vista dell'occupazione, perche' utilizzare oggetti immutabili significa anche che un'operazione su uno di essi si traduce in una creazione di una copia (proprio come per le stringhe, in Java). Ciò può essere vero per una elaborazione per cui si è assolutamente certi che non verrà mai introdotto parallelismo; ma non appena nascesse la necessità di parallelizzare qualche operazione, saremmo costretti a rivedere le strutture dati per introdurre delle sezioni critiche (che diventano molto penalizzanti se il numero di thread diventa elevato) oppure clonare collezioni mutabili da inviare ad altri thread (e creare una copia di una collezione mutabile è molto più dispendioso che inviare semplicemente un riferimento ad una collezione immutabile, che non necessiterebbe di essere clonata).

Dunque, utilizzando oggetti mutabili, il codice prodotto è meno scalabile, perche' richiede lock o copie pesanti, e meno modulare, perche' non può essere ragionevolmente riutilizzato così com'è in contesti diversi da quelli per cui è stato scritto (avrebbe poco senso utilizzare un algoritmo colmo di sezioni critiche in un contesto non parallelo). Pur essendo concetti noti da molto tempo, valeva la pena ribadirli per spiegare la scelta di favorire l'immutabilità.

Detto ciò anche se non esistono algoritmi che non si possano scrivere utilizzando variabili immutabili, in certi contesti potrebbe essere desiderabile riassegnare (per esempio per ragioni di performance all'interno in un ciclo): in tal caso si può usare la parola chiave var:

var a = 10
a = a + 1 // OK!

ma è da evitare, perche' spinge verso uno stile di programmazione imperativo che è la causa di un codice poco scalabile e sicuro. Sarebbe da adottare solo se effettivamente esistono ragioni precise per usarla.

Tipo non specificato

Un altro concetto che emerge è che nelle dichiarazioni non viene specificato il tipo, perche' il compilatore lo deduce da ciò che segue. Generalmente il compilatore è abbastanza intelligente da stabilire il tipo da se'. Qualora non fosse in grado di farlo, genera un errore di compilazione; se in qualche caso vogliamo essere assolutamente certi che una variabile sia proprio di un certo tipo, nessuno ci impedisce di specificarlo espressamente:

val a : Int = 10

Attenzione: il tipo viene dedotto a compile-time, non a run-time come invece nel caso dei linguaggi dinamici come Groovy. Ecco perche' Martin Odersky, il creatore di Scala, lo ha definito:

"una miscela di programmazione orientata agli oggetti e funzionale in un linguaggio staticamente tipato" [4].

Definizione delle funzioni

Un altro aspetto che vediamo nel Listato 1 è che abbiamo due modi di definire le funzioni, uno con val e uno con def. Non sono del tutto equivalenti: il secondo definisce un metodo con arietà 1 (prende un solo parametro) mentre il primo definisce un "function literal", una funzione anonima. Ci sono diversi modi per definire un function literal, ad esempio dispari poteva essere scritto anche così:

val dispari = (i:Int) => i % 2 == 1

oppure così:

val dispari : Int => Boolean = i => (i % 2 == 1)

La sintassi che abbiamo usato in Listato 1 è la più compatta e probabilmente la più usata, che sfrutta la capacità del compilatore di dedurre i tipi impiegati; comunque non ci sono controindicazioni nell'usare le altre.

Object Oriented Programming

Ancora, il Listato 1 sembra non aver nulla a che fare con OOP. In realtà, poiche' il compilatore permette di omettere il punto e le parentesi, la terza riga è del tutto equivalente a:

val mySquares = ((1.to(19)).filter(dispari).map(allaSeconda)

Ora è palese che abbiamo a che fare con oggetti, tutti immutabili; infatti, sia il metodo filter sia map restituiscono una nuova sequenza immutabile di interi (IndexedSeq[Int]). Se non specificato diversamente, Scala utilizza sequenze e contenitori immutabili con lo scopo di favorire l'utilizzo di questi invece dei contenitori mutabili, che pure sono disponibili ma devono essere richiesti in modo esplicito. Vedremo questi aspetti in dettaglio in seguito.

Questo esempio ci porta anche a riconoscere un'altra caratteristica di Java che lo rende a volte un po' ostico: la dicotomia tra tipi primitivi e oggetti. In Scala tutti i tipi, compresi i numeri, sono oggetti. Ma poiche' utilizzare i tipi primitivi solo boxed sarebbe poco performante, il compilatore, quando può, genera bytecode che utilizza quei dati come primitivi; altrimenti, se deve, li converte in oggetti. Quindi lo sviluppatore usa sempre Int: è compito del compilatore promuoverlo a Integer o utilizzare int a seconda dei casi.

NullPointerException

Un altro tipico problema con la maggior parte dei linguaggi che lavorano con riferimenti è la fatidica NullPointerException (NPE). Con scala è possibile evitarla utilizzando la monade Option. Ad esempio, supponiamo di aver implementato una funzione "cerca una stringa in una certa sorgente". In caso di fallimento, in Java potremmo restituire null, confidando che il client controlli il valore restituito (per semplicità ipotizziamo che la sorgente dati sia una mappa).

public String find(int id) {
       if(mySource.containsKey(id))
             return mySource.get(id);
       else
             return null;
}

Per compatibilità con la JVM, in Scala è possibile farlo allo stesso modo:

def find(id : Int) = if(mySource contains id) mySource(id) else null

Come si vede, l'unica comodità sembra essere quella di una scrittura più compatta e più leggibile, dovuta al fatto che possiamo omettere il punto, le parentesi, la parola chiave return (if può restituire un valore) e il tipo di ritorno che viene dedotto dal tipo di ritorno di mySource(id). Se per qualche variazione nel programma, la mappa non dovesse più contenere stringhe ma un tipo di oggetto più complesso, questo codice non necessiterebbe alcuna modifica: basterebbe ricompilarlo. Resterebbe il problema che dovremmo fidarci che il client esegua il test di nullità. Ma per essere sicuri a compile-time che il client controlli il valore di ritorno, possiamo usare Option:

def find(id : Int) : Option[String] = if(mySource contains id) Some(mySource(id)) else None

Questa funzione, dato un intero, cerca nella sorgente la stringa corrispondente e, se la trova, la restituisce boxed con Some, altrimenti restuitisce None, ossia niente. Qui abbiamo specificato il tipo di ritorno, ma solo per chiarezza, perche' Some e None creano istanze di Option quindi il compilatore è in grado di dedurlo in autonomia.

Ora il client non ottiene più un riferimento ad un oggetto che può essere nullo, ma un oggetto che o è qualcosa o rappresenta il vuoto. Vediamo come può essere utilizzato:

def getLength(id : Int) = find(id) match {
       case Some(a) => a.length
       case None => 0
}

Questa funzione ottiene la lunghezza della stringa con l'id specificato: essa dapprima lo cerca con find, se il valore di ritorno corrisponde a una qualche stringa, restituisce la lunghezza di quella stringa, se corrisponde a niente, restituisce zero.

Pattern Matching

Per fare questa cosa, il linguaggio utilizza il pattern matching, un concetto molto potente di Scala, su cui ritorneremo; per ora basti sapere che il codice qui sopra sostituisce un test di nullità su una variabile, ma in modo migliore, perche' è obbligatorio e controllato dal compilatore. Una cosa che notiamo è che la variabile a nella prima riga dei casi possibili viene istanziata col valore boxed all'interno di Some. Questa è una regola generale del pattern matching. Per esempio si possono fare cose del tipo:

def getLength(a : List[String]) : Int = a match {
       case head :: tail => head.length + getLength(tail)
       case Nil => 0
}

Questa funzione ricorsiva calcola la lunghezza totale delle parole incluse nella lista passata come parametro. Se la lista non è vuota, ha una testa (che è una stringa) e una coda (che è ancora una lista di stringhe, magari vuota); in tal caso la lunghezza è data dalla lunghezza della testa più il calcolo della lunghezza sul resto della lista. Se invece la lista è vuota (Nil), la lunghezza è zero. Notiamo che abbiamo specificato il tipo di ritorno (Int). Qui è necessario perche' in caso di funzioni ricorsive il compilatore ha bisogno di conoscere il tipo di ritorno.

Semplificazione della scrittura

Aggiungiamo che alcune parti del codice scritto fin qui, in realtà, sono superflue, perche' gli oggetti come le mappe e Option, hanno già dei metodi che permettono di semplificare ancora la scrittura; lo abbiamo scritto in questo modo per rendere chiari questi meccanismi.

Cambiare stile: da imperativo a funzionale

Abbiamo accennato al fatto che uno stile di programmazione funzionale e con oggetti immutabili è più scalabile e parallelizzabile di uno stile imperativo e con largo uso di riassegnazioni.

Ad esempio, vogliamo una funzione che, dato un elenco di stringhe, le scorre e le stampa. Con Scala possiamo scriverla in modo totalmente imperativo:

def printElements(elements : scala.collection.mutable.LinkedList[String]) {
       var i = 0;
       while(i < elements.length) {
             println("Elemento all'indice " + i + " = " + elements.apply(i));
             i = i + 1
       }
}

Osserviamo in questo codice che abbiamo un'istruzione while analoga a quella di Java; i è un'intero mutabile mentre elements è una lista mutabile (stile Java) di stringhe.

Un problema in questo codice è che un altro thread potrebbe accedere questa lista, quindi saremmo costretti a inglobarlo in una sezione critica, oppure a fare una copia della lista prima di stamparla. Un altro problema è che è difficile da testare e non può essere adattato ad altre situazioni perche' il metodo chiama direttamente println (una funzione corrispondente a System.out.println). In Java, per ovviare a ciò, definiremmo un'interfaccia e la metteremmo come argomento della funzione; in Scala si può fare allo stesso modo, ma anche meglio:

def printElements(elements : List[String], printFunc : String => Unit) {
      
       val strings = elements.zipWithIndex map (t => "Elemento all'indice " 
                                                + t._2 + " = " + t._1 )
      
       strings foreach(printFunc(_))
}

Essa prima di tutto prende una lista immutabile di stringhe, poi, dopo aver creato (zipWithIndex) una collezione di tuple [8] formata da elemento e indice corrispondente, la mappa su una stringa; quindi, non usa direttamente println nel codice ma, per ogni elemento esegue una funzione (foreach) che può essere passata dall'esterno, ad esempio in questo modo:

val names = List("Mokabyte", "Scala", "Article")
printElements(names, s => println(s))

Parametro implicito

Ma si può fare ancora meglio. Ipotizziamo di avere una funzione predefinita per la stampa e che solo raramente ne utilizziamo un'altra (per esempio in fase di test). Allora si può usare un parametro implicito:

def printElements(elements : List[String])(implicit printFunc : String => Unit) {
                
       val strings = elements.zipWithIndex.map(t => "Elemento all'indice " 
                                               + t._2 + " = " + t._1 )
                
       strings.foreach(printFunc(_))
}
implicit val defaultPrint : (String => Unit) = println(_ : String)
val names = List("Mokabyte", "Scala", "Article")
printElements(names)
printElements(names)(s => println("*** " + s))

Oltre alla parola chiave implicit, qui vediamo altre novità. Prima di tutto, abbiamo messo un'altra lista parametri. È come se printElements(List[String]) in realtà restituisse una funzione che prende un'altra lista di parametri ma che nel corpo può utilizzarli tutti e due. E per lo sviluppatore è proprio così. Funzioni così definite costituiscono uno strumento molto potente e sono dette "curried function"; le vedremo meglio in seguito. Con implicit prima di val abbiamo dichiarato la nostra funzione di stampa predefinita, ed è quella che viene usata nella prima chiamata di printElements; nella seconda chiamata, invece, poiche' passiamo un nuovo metodo per la stampa, viene usato quello. È evidente come questa caratteristica permetta una grande flessibilità e facilità di applicazione, ad esempio in un DSL.

Classi

Uno dei motivi per cui Java è verboso, è la necessità di dover spesso scrivere getter e setter per i campi privati di una classe, necessità che è data dal fatto che si potrebbe introdurre della logica in tali metodi, magari nelle classi figlie, oppure si potrebbe astrarre quelle classi (e nelle interfacce i campi non hanno senso). Generalmente si sopperisce con un buon ambiente di sviluppo, ma ciò non toglie che, per questo motivo, spesso i programmatori Java invidiano ad altri linguaggi come C# le proprietà, che possono incapsulare un campo oppure essere fatte di pura logica. Scala supera il problema in un modo ancora più elegante.

Innanzitutto un po' di sintassi. Poniamo di voler creare una classe che rappresenta una persona, per ora vuota. Scriviamo:

class Person

e questo è sufficiente, non serve altro. Una classe del genere sembrerebbe totalmente inutile, invece viene usata in taluni contesti come il pattern matching o per rappresentare messaggi nel modello di concorrenza con gli Actor come vedremo nei prossimi articoli. La nostra persona però, è un oggetto immutabile con un nome e un cognome:

class Person(val surname : String, val name : String)

Questa definizione significa che la classe ha un costruttore con due parametri che coincidono con gli unici due campi (immutabili). Per avere anche un campo opzionale per la data di nascita, dobbiamo fare così:

import org.joda.time._
 
class Person(val surname : String, val name : String, 
                       val birthdate : Option[DateMidnight]) {
       def this(surname : String, name : String) = this(surname, name, None)
}

Dove abbiamo usato JodaTime [3], le famose API Java di per la manipolazione del tempo. Come si vede è sufficiente usare la parola chiave import e mettere le librerie nel classpath del progetto, oppure se si usa l'interprete, nella directory lib, nel percorso d'installazione di Scala. Il simbolo _ indica che vogliamo importare tutto il package (il sistema per l'importazione è molto flessibile, lo vedremo man mano che se ne presenterà la necessità). La definizione this nella classe definisce un costruttore ausiliario, che richiama quello principale, impostando con None il valore della data opzionale.

A questo punto, visto che sappiamo come funziona Option, possiamo implementare anche una funzione che calcola l'età:

class Person(val surname : String, val name : String, 
                       val birthdate: Option[DateMidnight]) {
                
       def this(surname : String, name : String) = this(surname, name, None)
       def getAge() : Option[Int] = birthdate match {
             case Some(date) => Some(Years.yearsBetween(date, new DateTime()).getYears())
             case None => None
       }
}

Il metodo getAge restituisce: None, se anche la data di nascita è None, altrimenti l'età in anni della persona.

Ora, questo ultimo listato è corretto dal punto di vista sintattico, ma non rispetta alcune convenzioni di Scala. Queste dicono, infatti, che i metodi che non causano side-effect dovrebbero essere dichiarati (e invocati) senza le parentesi, come se fossero dei campi a tutti gli effetti:

class Person(val surname : String, val name : String,
             val birthdate: Option[DateMidnight]) {

                 def this(surname : String, name : String) = this(surname, name, None)

                 def age : Option[Int] = birthdate match {
             case Some(date) => Some(Years.yearsBetween(date, new DateTime).getYears)
             case None => None
       }
}

Tale linea guida serve per differenziare ciò che è puramente funzionale da ciò che non lo è, ed è utile per lo sviluppatore perche' sa se può invocare quel codice in uno stile di programmazione puramente funzionale oppure isolarlo. Infatti, tutte le funzioni che hanno un side effect andrebbero isolate per evitare che tali effetti siano sparpagliati nell'applicazione e rendano tutto il codice poco scalabile.

Avendo quindi anche l'età possiamo implementare un primo metodo toString:

class Person(val surname : String, val name : String,
             birthdate: Option[DateMidnight]) {
       
                 def this(surname : String, name : String) = this(surname, name, None)
       
                 def age : Option[Int] = birthdate match {
             case Some(date) => Some(Years.yearsBetween(date, new DateTime).getYears)
             case None => None
       }
       
                 override def toString = surname + " " + name + (age match {
             case Some(years) => ", di anni " + years
             case None => ""
       })
}

Quindi, proviamo a mettere tutto nell'interprete Scala, e avremo quello che è mostrato nella figura 1.

Figura 1 - Interprete Scala in azione.

 

Ora, il metodo age deve essere valutato ogni volta, perche' l'età della persona potrebbe cambiare se quel codice viene eseguito su un server che può rimanere acceso anche per anni. Non sempre è così. Supponiamo di estendere la nostra classe con un'altra, che chiamiamo FiscalPerson, avente anche un codice fiscale. Poiche' la data di nascita è già nel codice fiscale, la classe figlia non deve richiedere nel costruttore anche la data di nascita. Per fare ciò è sufficiente utilizzare override:

class FiscalPerson(surname : String,
                    name : String,
                    val codFisc: String
                    ) extends Person(surname, name) {
      
       override val birthdate = Some(
                           new DateMidnight(
                           1900 + Integer.parseInt(codFisc.substring(6, 8)),
                           codFisc.substring(8, 9)(0) - 'A',
                           Integer.parseInt(codFisc.substring(9, 11))
                           ))
}

La parola chiave extends funziona come in Java, tranne che permette di passare direttamente i parametri alla classe base. Oltre a questa infrastruttura, in questa classe c'è di nuovo che abbiamo dichiarato con val un campo che però viene calcolato. In questo caso il codice per calcolare il campo viene eseguito all'atto della costruzione della classe, quindi non viene valutato ogni volta: questa è la differenza tra val e def.

Adesso questo codice però presuppone che il codice fiscale passato sia valido. Se ciò non fosse, dobbiamo lanciare un eccezione al momento della costruzione della classe. Tutto il codice che si trova nella classe, che non sia dentro un metodo viene eseguito dal costruttore, quindi è sufficiente aggiungere la validazione nella classe:

class FiscalPerson(surname : String,
                    name : String,
                    val codFisc: String
                    ) extends Person(surname, name) {

                 if( ! CodiceFiscale.isValid(codFisc) )
             throw new IllegalArgumentException("codice fiscale non valido!")
          ...

Dove abbiamo presupposto che esiste un oggetto CodiceFiscale che permetta di validare la classe. Abbiamo detto oggetto, perche' in Scala non esistono metodi statici; esiste però il singleton, definito a livello di linguaggio. Cioè è possibile definire un oggetto che viene istanziato al primo utilizzo ed è garantito che ne esiste un'unica istanza nel programma, in questo modo:

object CodiceFiscale {

                 // ovviamente per brevità abbiamo omesso tutti gli altri controlli
       def isValid(cf : String) = (cf.length == 16)
}

Resta un'ultima cosa. Supponiamo di aggiungere un campo "comune" che restituisca il comune calcolato dal codice fiscale:

class FiscalPerson(surname : String,
                    name : String,
                    val codFisc: String
                    ) extends Person(surname, name) {

                 if( ! CodiceFiscale.isValid(codFisc) )
       throw new IllegalArgumentException("codice fiscale non valido!")

                 val birthPlace = CodiceFiscale.birthPlace(codFisc);

                 override val birthdate = Some(
                                  new DateMidnight(
                                  1900 + Integer.parseInt(codFisc.substring(6, 8)),
                                  codFisc.substring(8, 9)(0) - 'A',
                                  Integer.parseInt(codFisc.substring(9, 11))
                                  ))
}

          object CodiceFiscale {

                 def isValid(cf : String) = (cf.length == 16)

                 // ovviamente qui dovrebbe cercare il comune in un database
       // o una mappa
       def birthPlace (cf : String) = "Roma"
}

Supponiamo ora che i client di questa classe siano raramente interessati al luogo di nascita quindi interroghino raramente quel campo, allora possiamo farlo lazy, ossia "pigro": possiamo fare in modo che sia calcolato solo alla prima interrogazione. In Java, se fossimo stati previdenti e avessimo messo un getter, potremmo scrivere così e non cambierebbe nulla per il client:

private String birthPlace;
public getBirthPlace () {
       if(birthPlace == null) birthPlace = computeBirthPlace ();
       return birthPlace;
}

Se invece quella classe espone il campo pubblicamente, non possiamo fare altro che modificare anche i client. Con Scala, invece è sufficiente aggiungere la parola chiave lazy. Il campo verrà inizializzato alla prima interrogazione.

Concludiamo quindi riportando il listato per intero:

import org.joda.time._
 
object CodiceFiscale {
      
       // per brevità omettiamo tutti gli altri controlli
       def isValid(cf : String) = (cf.length == 16)
      
       // ovviamente qui dovrebbe cercare il comune in un database
       def birthPlace (cf : String) = "Roma"
}
 
class Person(val surname : String, val name : String,
             val birthdate: Option[DateMidnight]) {
      
       def this(surname : String, name : String) = this(surname, name, None)
      
       def age : Option[Int] = birthdate match {
             case Some(date) => Some(Years.yearsBetween(date, new DateTime).getYears)
             case None => None
       }
      
       override def toString = surname + " " + name + (age match {
             case Some(years) => ", di anni " + years
             case None => ""
       })
}
 
class FiscalPerson(surname : String,
                    name : String,
                    codFisc: String
                    ) extends Person(surname, name) {
      
       if( ! CodiceFiscale.isValid(codFisc) )
             throw new IllegalArgumentException("codice fiscale non valido!")
      
       lazy val birthPlace = CodiceFiscale.birthPlace(codFisc);
      
       override val birthdate = Some(
                                  new DateMidnight(
                                  1900 + Integer.parseInt(codFisc.substring(6, 8)),
                                  codFisc.substring(8, 9)(0) - 'A',
                                  Integer.parseInt(codFisc.substring(9, 11))
                                  ))
}

Conclusioni

In questo articolo abbiamo discusso alcuni motivi per cui è utile conoscere un nuovo linguaggio come Scala e abbiamo dato un veloce sguardo alle sue potenzialità che permettono di raggiungere un'eleganza e una produttività, le quali richiederebbero molto più impegno in Java. Non ci si preoccupi se non si è compreso appieno il significato di alcuni passi, ritorneremo su questi concetti nei prossimi articoli e soprattutto vedremo altri costrutti ancora più potenti; altrimenti, per gli impazienti, rimandiamo innanzitutto alla documentazione raggiungibile dal sito ufficiale [1], che è anche un ottimo aggregatore di notizie e articoli. Per approfondire, consigliamo il libro che vede Odersky come coautore [4], interessante anche perche' spiega le motivazioni di certi costrutti presenti in Scala, anche dal punto di vista della JVM e del compilatore. Tra i numerosi blog che si occupano di Scala e in generale di altri linguaggi funzionali, segnaliamo quelli di Daniel Spiewak [5], di James Iry [6] e di Debasish Ghosh [7]. Alla prossima!

Riferimenti

[1] The Scala Programming Language

http://www.scala-lang.org/

 

[2] Martin Odersky, biografia su Ecole Polytechnique Federale De Lausanne (EFPL)

http://people.epfl.ch/martin.odersky

 

[3] JodaTime, Java Date and Time API

http://joda-time.sourceforge.net/

 

[4] Martin Odersky, Lex Spoon, Bill Venners, "Programming in Scala, Second Edition. A comprehensive step-by-step guide", Artima

 

[5] Code Commit

http://www.codecommit.com

 

[6] James Iry Blog

http://james-iry.blogspot.com

 

[7] Ruminations of a Programmer

http://debasishg.blogspot.com

 

[8] Tuple (inglese)

http://en.wikipedia.org/wiki/Tuple

Condividi

Pubblicato nel numero
163 giugno 2011
Onofrio Panzarino, ingegnere elettronico, lavora ad Ancona come software architect, per Wolters Kluwer Italia. Sviluppatore con esperienza in vari linguaggi e piattaforme, soprattutto web-oriented, è molto interessato a soluzioni scalabili e a linguaggi di programmazione funzionali. È speaker in JUG Marche su argomenti correlati a Scala e database NoSQL.
Articoli nella stessa serie
Ti potrebbe interessare anche