Scuola Politecnica e delle Scienze di Base Corso di Laurea in Ingegneria Informatica Elaborato finale in Programmazione I Caratteristiche fondamentali del linguaggio di programmazione Scala Anno Accademico 2015/2016 Candidato: Davide Casillo matr. N46001379 [Dedica] Indice Indice .................................................................................................................................................. III Introduzione ......................................................................................................................................... 4 Capitolo 1: Programmazione Funzionale ............................................................................................. 5 1.1 Caratteristiche principali ............................................................................................................ 5 1.2 Benefici della programmazione funzionale ............................................................................... 7 1.3 Evoluzione dei linguaggi di tipo funzionale .............................................................................. 7 Capitolo 2: Scala .................................................................................................................................. 9 2.1 Modalità di esecuzione............................................................................................................. 10 2.2 Sintassi ..................................................................................................................................... 11 2.2.1 Variabili e costanti ....................................................................................................... 11 2.2.2 Definizione delle classi ................................................................................................ 13 2.2.3 Tratti ............................................................................................................................. 16 2.3 Costrutti principali ................................................................................................................... 18 2.3.1 Costrutto if ................................................................................................................... 18 2.3.2 Costrutto for ................................................................................................................. 18 Capitolo 3: Scala - Aspetti Avanzati .................................................................................................. 20 3.1 Gerarchia dei tipi ...................................................................................................................... 20 3.2 Ambiti di visibilità ................................................................................................................... 22 3.3 Uguaglianza tra oggetti ............................................................................................................ 23 3.4 Strutture Dati ............................................................................................................................ 25 3.4.1 Tuple ............................................................................................................................ 25 3.4.2 Liste .............................................................................................................................. 26 3.4.3 Vettori .......................................................................................................................... 27 3.4.4 Collezioni parallele ...................................................................................................... 28 3.4.5 Mappe........................................................................................................................... 28 3.5 Cenni sulla programmazione concorrente................................................................................ 29 Appendice: Esempi di codice ............................................................................................................. 31 Client – Server ............................................................................................................................... 31 Conteggio parole ............................................................................................................................ 33 Conclusioni ........................................................................................................................................ 34 Bibliografia ........................................................................................................................................ 36 Introduzione Il seguente elaborato descriverà quali sono le caratteristiche fondamentali del linguaggio di programmazione Scala. Sono molte le società che negli ultimi anni stanno adoperando Scala come linguaggio principale nello sviluppo di nuovo software e altre ancora stanno migrando verso tale linguaggio riscrivendo gran parte di software già esistente. Grandi compagnie come Google oppure Apple hanno scelto di formare team di sviluppatori Scala per progetti futuri oppure Twitter che ha deciso di abbandonare Ruby convertendo tutta la piattaforma in Scala. L’enorme successo che sta vivendo Scala è dovuto alle sue caratteristiche che verranno descritte nel corso di tale elaborato e soprattutto alla sua capacità di fornire le migliori prestazioni nel caso di sistemi concorrenti e distribuiti. Nel primo capitolo si effettuerà una panoramica sulla programmazione funzionale analizzando le sue caratteristiche principali, i benefici e la sua evoluzione nel tempo. Nel secondo capitolo saranno descritti i costrutti principali del linguaggio di programmazione Scala e i suoi aspetti sintattici. Infine, nel terzo capitolo, si analizzeranno gli aspetti avanzati di Scala riguardante la componente Object Oriented e funzionale. 4 Capitolo 1: Programmazione Funzionale Come ben noto lo sviluppo dei sistemi software nel tempo è diventato sempre più complesso, per cui risulta sempre più importante strutturarli nel modo migliore. Software ben strutturati sono facili da scrivere e permettono di avere una collezione di moduli che possono essere utilizzati nuovamente per ridurre il costo di sviluppo. Nel seguente capitolo verrà spiegato perché la programmazione funzionale, attraverso le sue caratteristiche, contribuisce allo sviluppo modulare. 1.1 Caratteristiche principali La programmazione funzionale è uno stile fondamentale di programmazione in cui l’esecuzione di un programma avviene attraverso lo svolgimento di espressioni matematiche e l’utilizzo di variabili immutabili. Caratteristica necessaria della programmazione funzionale è quella di avere funzioni di tipo “first-class”, precisamente “higher-ordered”, ovvero funzioni che possono essere utilizzate come argomento o come valore di ritorno di un’altra funzione. Un esempio di funzione higher-ordered è la funzione MAP la quale ha come argomenti una funzione e una lista e ritorna come valore la lista trasformata applicando la funzione ad ogni elemento della lista. (map double (list 1 2 3 4 5)) => (2 4 6 8 10) La funzione double è applicata alla lista di interi (1,2,3,4,5) e restituisce la lista con i 5 valori raddoppiati. Programmi di tipo funzionale non hanno “effetti collaterali”, questo significa che l’esecuzione di una funzione produce solo il risultato delle sue operazioni senza modificare lo stato di altre funzioni. Questa caratteristica è molto importante perché elimina il verificarsi di molti bug e, inoltre, rende irrilevante l’ordine di esecuzione delle varie funzioni all’interno del programma in quanto ogni funzione è indipendente dall’altra. Quindi, a valle di queste considerazioni, risulta molto più semplice verificare il corretto funzionamento del software attraverso le operazioni di testing. Dal momento in cui le espressioni possono essere valutate in qualsiasi momento, si possono liberamente rimpiazzare le variabili con il loro valore e viceversa - ovvero, i programmi godono della proprietà di “trasparenza referenziale”. Si ricorda che per trasparenza referenziale si intende che una funzione fornisce lo stesso risultato, dati gli stessi input, in tutte le istanze. Questa libertà aiuta a rendere programmi funzionali matematicamente più trattabili rispetto ai loro omologhi convenzionali. Un’altra importante proprietà è la “lazy evaluation” (valutazione pigra) che consiste nell’eseguire una determinata operazione solo quando è realmente necessaria. Ad esempio se dobbiamo applicare la funzione MAP su una lista di elementi, questa verrà eseguita solo quando tutti gli elementi della lista sono necessari per altre operazioni. Questa proprietà permette di: definire strutture dati potenzialmente infinite; aumentare le performance evitando di eseguire operazioni non richieste; definire strutture di controllo come astrazioni invece che come primitive; Ampliamente utilizzata è la “Tail Call Optimization” che permette di non occupare ulteriore spazio nello stack quando c’è una chiamata a funzione. Questo è possibile quando l’ultima operazione di una funzione è quella di chiamare sé stessa in modo ricorsivo. Nella programmazione funzionale, infatti, le iterazioni avvengono attraverso l’uso della ricorsione. Funzioni ricorsive non fanno altro che richiamare sé stesse finché non è raggiunto il caso base. Quindi grazie alla tecnica della “Tail Call Optimization” si rende più efficiente la ricorsione e soprattutto evita che si crei uno scenario di stack 6 overflow. È importante notare come il problema della concorrenza viene risolto senza l’utilizzo di particolare tecniche come ad esempio semafori o monitor. Infatti basandoci sulla condizione di immutabilità delle variabili non si presenta più il problema della race condition. 1.2 Benefici della programmazione funzionale Le caratteristiche introdotte nel paragrafo precedente permettono di utilizzare la programmazione funzionale per sviluppare programmi più strutturati rispetto alla controparte imperativa. Per creare un programma strutturato è necessario sviluppare delle astrazioni e suddividere il programma in più componenti che si interfacciano tra di loro attraverso tali astrazioni. I linguaggi funzionali raggiungono perfettamente l’obiettivo rendendo semplice la creazione di tali astrazioni. È facile, ad esempio, astrarre righe di codice ricorrenti creando una funzione di tipo high-order che permette di ottenere un codice più dichiarativo e comprensibile. 1.3 Evoluzione dei linguaggi di tipo funzionale La nascita della programmazione funzionale è strettamente legata al “Lambda Calcolo”, un ramo della logica nato tra gli anni ’20 e ’30. Il Lambda Calcolo è stato sviluppato da logici (la prima versione fu definita da Church nel 1932) con l’obiettivo di scoprire come definire le funzioni in modo formale e come usare questo formalismo come base per la matematica. Esso rappresenta un semplice linguaggio formale di funzioni così potente da poter definire quasi tutta la matematica conosciuta. Ovviamente tale linguaggio non è stato definito per scopi informatici anche perché non esisteva nessun tipo di computer o calcolatore. Successivamente, nel 1960, nasce il primo linguaggio funzionale con l’introduzione della prima versione di LISP da parte di McCarthy. Negli anni sono nati molti dialetti di tale 7 linguaggio LISP come ad esempio Scheme e il Common LISP. LISP, ad oggi, è ampliamente utilizzato come linguaggio per l’Intelligenza Artificiale, ad esempio nel controllo real-time di robots e come macro linguaggio per software come AutoLISP per Autocad. Alla fine degli anni ’70 Backus esprime la sua idea sulla realizzazione di un linguaggio di programmazione puro e high-order con una sintassi molto vicina alla logica combinatoria. Egli definisce così il linguaggio FP (Function Programming) che ha molto influenzato lo sviluppo dei linguaggi funzionali. Elemento principale di FP sono le funzioni di tipo highorder. Nello stesso periodo, nell’università di Edimburgo, è stato definito il linguaggio ML (Meta-Language) per esprimere strategie di ricerca tramite il theorem prover LCF. Eredita da FP le funzioni di tipo high-order introducendo importanti novità come la deduzione automatica del tipo ed è utilizzato soprattutto in compilatori, analizzatori e theorem provers. Essendo general purpose, però, trova applicazione anche in bioinformatica oppure in sistemi finanziari. Come LISP anche ML possiede molti dialetti, i più diffusi sono SML (Standard ML) e CAML. Nel 1985 David Turner sviluppa Miranda, un linguaggio di programmazione molto simile a ML. È uno dei primi linguaggi a possedere la proprietà della lazy evaluation. Nel 1986 Ericsson introduce Erlang, un linguaggio di programmazione funzionale che lavora con processi concorrenti. Inizialmente è stato utilizzato per applicazioni telematiche ma è diventato un linguaggio general purpose. Infatti Erlang rappresenta il linguaggio funzionale più utilizzato nel mondo dell’industria. Infine nel 1990 nasce Haskell con l’obiettivo di diventare lo standard dei linguaggi di programmazione funzionale. È adottato in vari scenari come quello Aerospaziale, difesa e finanza. 8 Capitolo 2: Linguaggio di programmazione Scala Lo sviluppo di Scala, che sta per “Scalable Language”, inizia nel 2001 da parte di Martin Odersky insieme ai ricercatori dell’École Polytechnique Fédérale de Lausanne (EPFL). Dopo il rilascio di una prima versione interna nel 2003, la prima versione pubblica di Scala fu annunciata nel 2004, prima su piattaforma Java e successivamente su quella .NET. Nel 2006 Scala raggiunge la seconda versione mentre nel 2012 viene ufficialmente abbandonato il supporto alla piattaforma .NET. Il 17 Gennaio 2012 il team sviluppatore di Scala ha ricevuto un finanziamento di 2.3 milioni di euro dal Consiglio Europeo della Ricerca. Scala è un linguaggio di programmazione multi-paradigma che unisce la programmazione ad oggetti con quella funzionale. È un linguaggio ad oggetti perché ogni elemento è un oggetto. Il tipo e il comportamento degli oggetti è descritto attraverso le classi. Il design pattern creazionale Singleton è supportato per la creazione di oggetti mentre il design pattern comportamentale Visitor viene utilizzato attraverso il pattern matching. Attraverso l’utilizzo di classi implicite è possibile estendere classi esistenti con nuove funzioni. Scala, inoltre, è stato progettato per lavorare con linguaggi meno puri ma orientati agli oggetti come Java. Infine esso è un linguaggio funzionale perché ogni funzione è vista come un valore e quindi l’annidamento, funzioni di tipo high-order e strutture dati immutabili sono perfettamente supportate. A differenza di altri linguaggi funzionali, Scala permette di ottenere uno stile di programmazione più funzionale in modo facile e graduale. Infatti, con una crescente esperienza e familiarità con il codice, si può facilmente sostituire la mutabilità dei software che si sviluppano con pattern funzionali molto più sicuri. 9 Scala permette di avere le migliori perfomance con software per server scalabili che fanno uso di processi concorrenti e sincronizzati, per il parallelismo di architetture multi core e di processi distribuiti nel cloud. 2.1 Modalità di esecuzione Una volta installato, Scala permette di eseguire un programma in due modalità differenti: interattiva e script. In modalità interattiva è possibile eseguire istruzioni tramite una shell interattiva fornita direttamente dall’ambiente Scala. Aprendo un terminale (in questo esempio si sta utilizzando un ambiente Windows) e scrivendo la seguente istruzione si può osservare quale versione di Scala è installata: Infine si utilizza l’istruzione println per stampare una stringa: Invece, in modalità script, sfruttando un qualsiasi editor di testo si crea un file con estensione “.scala” come mostrato di seguito: object HelloWorld { def main(args: Array[String]) { println("Hello, world!") } } 10 Si può facilmente compilare il programma Scala appena salvato attraverso il comando scalac che salverà nuovi file con estensione ”.class” nella cartella del programma tra cui uno chiamato ”nomeprogramma.class” che rappresenta il bytecode che verrà eseguito dalla Java Virtual Machine (JVM) attraverso il comando scala. 2.2 Sintassi Nel seguente paragrafo saranno descritti gli aspetti sintattici più importanti di Scala come la definizione di variabili, classi e tratti. È interessante notare che in Scala l’uso del punto e virgola come terminatore di linea di codice è a discrezione del programmatore perché il compilatore è in grado di determinare automaticamente la fine della riga come la fine dell’istruzione. 2.2.1 Variabili e costanti Scala permette di definire le variabili attraverso l’uso di due parole chiave: var instanzia una variabile che può cambiare valore ed è definita come variabile mutabile; val instanzia una variabile che non può cambiare valore ed è definita come variabile immutabile. Nel primo caso la sintassi è la seguente: var Nome : String = "Davide" mentre nel secondo caso: val Età : Int = 23 In generale per poter dichiarare una variabile in Scala si usa la seguente notazione: var o var Nome_Variabile : Tipo = [Valore Iniziale] 11 Se invece non si vuole associare un valore iniziale alla varabile appena dichiarata si può utilizzare la seguente sintassi: var Nome : String; val Età : Int; Quando si assegna un valore iniziale ad una variabile il compilatore di Scala riesce ad individuare il tipo di variabile dichiarata in base al valore assegnato, questo perché Scala supporta la type inference (inferenza di tipo). In questo modo l’esempio precedente può essere scritto come segue: var Nome = "Davide"; val Età = 23; Automaticamente il compilatore riconosce il tipo della variabile “Nome” come String e della variabile “Età” come Int. Se ad esempio si vuole dichiarare una variabile di tipo double utilizzando la type inference basta scrivere nel seguente modo: val Età = 23.0; Da notare che un’istruzione come quella riportata in seguito, inserita dopo la precedente riga di codice, comporta un errore in fase di compilazione perché la variabile “Età” essendo dichiarata come val è immutabile: Età = 35.0; Se invece si vuole dichiarare una variabile di tipo float si utilizza la seguente notazione: var Peso = 70.0f; 12 Si presenta un piccolo esempio di codice che spiega in che modo avviene la dichiarazione di variabili in Scala: object Esempio { def main(args: Array[String]) { var myVar :Int = 10; val myVal :String = "Hello Scala con dichiarazione di tipo."; var myVar1 = 20; val myVal1 = "Hello Scala senza dichiarazione di tipo."; println(myVar); println(myVal); println(myVar1); println(myVal1); } } 2.2.2 Definizione delle classi Si presenta un semplice esempio per capire in che modo avviene la definizione delle classi in Scala. La seguente classe definisce due variabili x e y e un metodo move, il quale non ritorna alcun valore. Il nome della classe svolge la funzione di costruttore a cui vengono passati un certo numero di parametri: nel nostro esempio tali parametri sono xc e yc. class Point(xc: Int, yc: Int) { var x: Int = xc var y: Int = yc def move(dx: Int, dy: Int) { x = x + dx y = y + dy println ("Posizione del punto x : " + x); println ("Posizione del punto y : " + y); } } La definizione delle funzioni in Scala avviene attraverso la seguente sintassi: def nomeFunzione ([lista di parametri]) : [return tipo] = { corpo della funzione return [espressione] 13 } Sia il tipo di ritorno che la lista di parametri sono opzionali in Scala. Una funzione che non ha nessun tipo di ritorno prende il nome di procedura e utilizza il tipo Unit come ritorno, tipo equivalente al void in Java. Una classe esistente può essere estesa utilizzando la parola chiave extends ma con due restrizioni: in primo luogo il metodo dell’override richiede l’uso della parola chiave override e come seconda restrizione solo il costruttore primario può passare parametri al costruttore della classe originale. Si presenta un esempio di estensione di una classe utilizzando la classe Point precedentemente dichiarata creando una nuova classe estesa Location. Attraverso la parola chiave extends è possibile ereditare da Point tutti i membri non privati e di creare il tipo Location come sottotipo di Point. In questo modo la classe Point prende il nome di superclasse mentre Location quello di sottoclasse. Scala permette l’ereditarietà da una sola superclasse. 14 class Point(xc: Int, yc: Int) { var x: Int = xc var y: Int = yc def move(dx: Int, dy: Int) { x = x + dx y = y + dy println ("Posizione del punto x : " + x); println ("Posizione del punto y : " + y); } } class Location(override val xc: Int, override val yc: Int, val zc :Int) extends Point(xc, yc){ var z: Int = zc def move(dx: Int, dy: Int, dz: Int) { x = x + dx y = y + dy z = z + dz println ("Posizione del punto x : " + x); println ("Posizione del punto y : " + y); println ("Posizione del punto z : " + z); } } object Esempio { def main(args: Array[String]) { val loc = new Location(10, 20, 15); loc.move(10, 10, 5); } } 15 2.2.3 Tratti Uno strumento molto potente che offre Scala agli sviluppatori sono i Tratti i quali permettono di creare nuove classi costituite da metodi appartenenti ad altre classi. Essi rappresentano l’alternativa di Scala all’ereditarietà multipla offrendo una soluzione migliore rispetto alle classi astratte e le interfacce utilizzate in Java in quanto queste ultime presentano delle limitazioni. Infatti le interfacce non contengono l’implementazione dei metodi che definiscono mentre una nuova classe non può estendere più classi astratte. Si presenta un semplice esempio di utilizzo dei Tratti in cui si definisce una classe astratta “Volatile” e i tratti “Nuoto” e “Volo” per definire vari tipi di uccelli che possono sia nuotare che volare o solo una delle due. abstract class Volatile trait Nuoto { def nuotare() = println(“Sto Nuotando”) } trait Volo { def messaggio : String def volare() = println(“messaggio”) } class Pinguino extends Volatile with Nuoto class Fregata extends Volatile with Volo { val messaggio = “Posso solo volare” } class Piccione extends Volatile with Nuoto with Volo { val messaggio = "Sono un buon volatile" } class Falco extends Volatile with Nuoto with Volo { val messaggio = "Sono un eccellente volatile" } Tipico problema dell’ereditarietà multipla è il Diamond Problem che Scala risolve attraverso la linearizzazione. Il Diamond Problem è un’ambiguità che si verifica quando due classi B e C ereditano da una classe A e una classe D eredita sia da B e C. Se D invoca un metodo definito in A da quale classe viene ereditato? 16 Si ricrea la seguente situazione in Scala utilizzando i Tratti. abstract class A{ def stato() : Boolean } trait B extends A{ override def stato() : Boolean = true } trait C extends A{ override def stato() : Boolean = false } Se viene istanziata una nuova classe D che estende sia B che C il metodo ritornerà true o false? Il risultato dipende dall’ordine in cui estendiamo la classe con i tratti. Infatti se la classe D è istanziata nel seguente modo il metodo ritornerà false. val D = new A with B with C{ println(d.stato) } Invece se istanziata in modo diverso ritornerà true. val D = new A with C with B{ println(d.stato) } 17 2.3 Costrutti principali Nel seguente paragrafo saranno illustrati in che modo funzionano in Scala i tipici costrutti della programmazione come le istruzioni condizionali e diversi tipi di cicli. 2.3.1 Costrutto if Nei linguaggi di programmazione un’istruzione condizionale può solo controllare l’esecuzione del codice senza produrre alcun valore, in Scala, invece, l’istruzione if è un’espressione e in quanto tale può ritornare un singolo valore. Ciò significa che una generica istruzione if del tipo: if(a == b) result = “E’ vero!” else result = “Non è vero!” può essere riscritta in questo modo: result = if(a == b) “E’ vero!” else “Non è vero!” 2.3.2 Costrutto for Scala offre strumenti avanzati per la gestione dei cicli for come il filtering e lo yielding. Prima di analizzare in dettaglio tali strumenti si presenta un generico esempio dell’istruzione for in Scala in cui sono istanziati una classe Persona e una lista di oggetti di tipo Persona: class Persona(val nome: String, val femmina: Boolean) val popolazione = List( new Persona("Davide Casillo", false), new Persona("Giuseppe Parente", false), new Persona("Gabriella Esposito", true), new Persona("Simona Castaldi", true), new Persona("Mario Casillo", false) new Persona("Francesca Casillo", true)) 18 Per poter stampare a video tutti gli elementi della lista basterà scrivere la seguente istruzione: for(Persona <- popolazione) println(person.nome); Uno strumento molto utile che offre Scala è il filtering che permette di filtrare gli oggetti di una lista per estrarre solo gli elementi che soddisfano il parametro di ricerca. Riprendendo l’esempio precedente si applica tale strumento alla lista di Persone per ricavare tutti gli uomini di cognome “Casillo”: for(persona: Persona <- popolazione if !persona.femmina if persona.nome.contains("Casillo")) println(persona.nome) Un altro importante strumento è lo yielding che permette di creare una nuova lista, partendo da una già esistente, applicando un nuovo algoritmo o funzione agli elementi estratti. Sempre dall’esempio precedente si può creare una nuova lista denominata “Nomi” che contenga solo i nomi delle persone: val nomi = for(Persona <- popolazione)yield persona.nome for(nome <- nomi) println(nomi) 19 Capitolo 3: Scala - Aspetti Avanzati Nel seguente capitolo saranno mostrati in dettaglio alcune caratteristiche di Scala riguardante la sua componente Object Oriented e ponendo l’attenzione soprattutto sulla parte funzionale. 3.1 Gerarchia dei tipi In Scala ogni valore è un oggetto sia che esso sia numerico o una funzione. Inoltre essendo Scala un linguaggio basato su classi si ha che ogni valore è un’istanza di una classe. La figura che segue mostra in che modo è configurata la gestione delle classi in Scala: 20 La superclasse da cui derivano tutte le classi di Scala è Any che definisce metodi final come ad esempio !=, ==, asIstanceOf[T] (per la conversione di tipo). Possiede due dirette sottoclassi che sono AnyVal e AnyRef che rappresentano rispettivamente la classe che contiene tutti i tipi valore e la classe a cui appartengono tutti i tipi riferimento. La prima, AnyVal, possiede tutte istanze di valore immutabile e nessun tipo può essere istanziato con la parola chiave new. Invece AnyRef rappresenta l’equivalente della classe Object di Java da cui derivano tutte le classi appartenenti a Java. Un’altra sottoclasse importante di AnyRef è ScalaObject da cui derivano tutte le classi fornite dalla piattaforma Scala come ad esempio List, Seq, Option. Alla base della figura si trova la classe Null che rappresenta il valore nullo solo per i tipi appartenenti a AnyRef mentre Nothing lo è per i tipi appartenenti a AnyVal. In realtà essendo Nothing una derivazione di tutte le classi esso può essere esteso anche ai tipi appartenenti alla classe AnyVal. Altre classi importanti che non sono rappresentante in figura sono Nil e Option. La prima viene utilizzato per istanziare una lista vuota: var lista : List [Int] = Nil La seconda, invece, è utile quando bisogna istanziare un valore che non è ancora definito in modo che lo sviluppatore non utilizzi null evitando di causare un errore del tipo NullPointerException come accade spesso in Java. La classe Option può assumere solo due valori: None che indica un valore assente e Some, invece, che contiene un certo valore. Con la seguente riga di codice si istanzia una variabile x di tipo String con nessun valore assegnato: var x : Option[String] = None 21 3.2 Ambiti di visibilità Rispetto ad altri linguaggi di programmazione, come ad esempio Java, Scala offre un numero maggiore di ambiti di visibilità che permettono di gestire in modo più dettagliato l’accesso degli identificatori all’interno del programma. Gli ambiti di visibilità proposti sono mostrati di seguito in ordine dal “più restrittivo” al “più aperto”: object-private: rappresenta la modalità di accesso più restrittiva in quanto il metodo così dichiarato sarà disponibile solo per l’istanza corrente dell’oggetto considerato. Altre istanze della stessa classe non possono accedere al metodo. Per poter dichiarare un metodo come object-private è necessario utilizzare la parola chiave private[this] come mostrato nell’esempio seguente: private[this] def myFunction = true private: il metodo è accessibile solo dalla classe in cui è stato definito e da tutte le altre istanze della stessa classe. Rappresenta l’equivalente private di Java. Essendo privato il metodo non è disponibile alle sottoclassi della classe in cui è definito. Affinché si possa dichiarare un metodo come privato bisogna anteporre la parola chiave private: private def myFunction = true protected: il metodo è accessibile solo dalla classe in cui è definito e dalle sue sottoclassi. Mentre in Java un metodo protected è accessibile da altre classi appartenenti allo stesso package, in Scala questo non è possibile rendendo la modalità protected più restrittiva. Si utilizza la parola chiave protected per dichiarare un metodo come protetto: protected def myFunction = true 22 package: affinché un metodo sia disponibile per tutte le classi appartenenti allo stesso package Scala mette a disposizione la modalità di accesso package. Si utilizza la parola chiave private[packageName] come mostrato nell’esempio seguente: package model{ class Università{ private[model] def myCampus{} } } public: se non è utilizzata alcuna parola chiave nella definizione del metodo questo è da ritenersi pubblico e quindi accessibile da tutte le classi di qualsiasi package: def myFunction = true 3.3 Uguaglianza tra oggetti In Scala gli operatori “==” e “!=” effettuano un controllo per valore oppure per riferimento in base al tipo di variabile che devono confrontare, al contrario di Java in cui eseguono un controllo per riferimento indipendentemente dal tipo di variabile. Entrambi gli operatori sono definiti come final (cioè non possono essere sovrascritti in una sottoclasse o tratto) nella classe Any e utilizzano il metodo equals, che è sempre definito nella classe Any ma non come final. Quando Scala deve confrontare due variabili di tipo di valore utilizza l’uguaglianza naturale (numerica o booleana) effettuando, quindi, un controllo per valore. Nel seguente esempio si può notare la differenza nella gestione dell’uguaglianza di variabili di tipo di valore prima in Scala e poi in Java: //Scala var str1 = "Davide" var str2 = "Davide" println(str1 == str2) //Ritorna true //Java String s1 = new String("Gabriella"); String s2 = new String("Gabriella"); System.out.println(s1 == s2); //Ritorna false 23 Invece quando bisogna confrontare variabili di tipo di riferimento gli operatori == e != si comportano come alias del metodo equals appartenente al java.lang.Object. Quindi, in questi casi, Scala effettua un controllo per riferimento. Affinché sia possibile utilizzare questi operatori nel modo che ci si aspetta con tipi definiti dall’utente è necessario sovrascrivere il metodo equals per assicurare che confronti i valori nel modo giusto. Si supponga di avere una classe Persona con un solo parametro “nome” di tipo stringa e due istanze di tale classe con lo stesso nome. Utilizzando l’operatore == oppure il metodo equals per il confronto del nome ci si aspetta un risultato positivo, ma non è così. class Persona(val nome: String) var persona1 = new Persona("Davide") var persona2 = new Persona("Davide") persona1 == persona2 //Dovrebbe essere true ma ritorna false Si può risolvere tale inconveniente andando a sovrascrivere il metodo equals: class Persona(val nome: String) { override def equals(that: Any) : Boolean = { that.isInstanceOf[Persona] && (this.hashCode() == that.asInstanceOf [Persona].hashCode()); } override def hashCode = name.hashCode } var persona1 = new Persona("Davide") var persona2 = new Persona("Davide") persona1 == persona2 //Vero Un’alternativa all’override del metodo equals (oppure toString o hashCode) è quella di utilizzare classi definite con la parola chiave case definite case class per le quali sono già implementati i metodi equals e hashCode 24 senza bisogno di sovrascrittura. 3.4 Strutture Dati Nel seguente paragrafo verranno descritte le principali strutture dati che Scala offre ai propri sviluppatori. La prima struttura dati analizzata sarà la tupla che rappresenta la struttura dati più semplice tra quelle descritte. Successivamente si passerà alle liste che permettono di avere ottime prestazioni con operazioni di accesso agli elementi in testa. In seguito saranno approfondite strutture dati più complesse come i vettori e le collezioni parallele che garantiscono migliori prestazioni nella gestione di dati di grandi dimensioni. Infine saranno descritte le mappe definite anche come Hash table. 3.4.1 Tuple Un tupla rappresenta una collezione di due o più valori in cui ogni valore può essere di tipo diverso rispetto agli altri elementi appartenenti alla stessa tupla. Si può istanziare una tupla utilizzando la seguente sintassi: val t = new Tuple3(28, "Davide", 7.5) La variabile t è una tupla contenente tre elementi di tipo intero, stringa e float. In alternativa si può utilizzare una forma ridotta come nell’esempio seguente: val twoInts = (3,9) val intAndString = (7, "Casillo") Si possono istanziare tuple di massimo 22 elementi quindi se si necessita di maggior spazio bisogna utilizzare altri tipi di strutture dati come vedremo nei paragrafi successivi. Il metodo che permette di accedere agli elementi di una tupla è il seguente: val t = (1,2,3,4) println(“Stampo il primo elemento della tupla:”+ val somma = t._1 + t._2 + t._3 + t._4 println(“Stampo la somma:” + somma) 25 t._1) 3.4.2 Liste La lista rappresenta una collezione di elementi ordinati molto più performante della tupla perché non ha limiti di dimensione e supporta funzioni che permettono di eseguire operazioni su tutti i suoi elementi. La sintassi per creare una lista è la seguente: val lista = List[tipo] Si possono creare liste di qualsiasi tipo come mostrato nell’esempio seguente: // Lista di stringhe val frutta: List[String] = List("uva", "anguria", "pesca") // Lista di interi val numeri: List[Int] = List(1, 2, 3, 4) // Lista vuota val vuota: List[Nothing] = List() Tutte le liste possono essere definite utilizzando due elementi fondamentali: una testa definita dall’operatore :: e una coda Nil che viene utilizzata anche per definire una lista vuota. Tutte le liste create nell’esempio precedente possono essere così ridefinite: val frutta = "uva" :: ("anguria" :: ("pesca" :: Nil)) val numeri = 1 :: (2 :: (3 :: (4 :: Nil))) val vuota = Nil Le operazioni basilari che possono essere effettuate sono: head: restituisce il primo elemento della lista. tail: restituisce l’ultimo elemento della lista. isEmpty: ritorna true se la lista è vuota altrimenti false. Attraverso l’operatore ::: oppure i metodi List.:::() o List.concat() è possibile unire due o più liste insieme. Si presenta un esempio: val frutta1 = "uva" :: ("anguria" :: ("pesca" :: Nil)) val frutta2 = "melone" :: ("mela" :: ("banana" :: Nil)) var fruit = frutta1 ::: frutta2 frutta = frutta1.:::(frutta2) frutti = List.concat(frutta1, frutta2) 26 Altre funzioni sono lenght() che ritorna il numero di elementi appartenenti alla lista oppure reverse() che inverte l’ordine di posizione degli elementi all’interno della lista. 3.4.3 Vettori L’utilizzo di una lista è molto efficiente quando bisogna lavorare con algoritmi che prelevano solo elementi in testa dato che il loro accesso, aggiunta o rimozione impiega sempre un tempo costante. Invece quando gli elementi da modificare o accedere non sono in testa il tempo di esecuzione aumenta in modo lineare in base alla loro posizione all’interno della lista. Per ovviare all’inefficienza dell’accesso casuale nelle liste è stata introdotta la struttura dati vettore che permette di accedere ad ogni suo elemento in un tempo costante indipendentemente dalla sua posizione. Si presenta la sintassi da utilizzare per creare un nuovo vettore: val vettore = Vector(23,24) Per poter aggiungere elementi in testa si utilizza l’operatore +: val vettore2 = 3 +: vettore Invece per aggiungere elementi in coda si fa uso dell’operatore :+ val vettore3 = vettore2 :+ 4 Essendo i vettori una struttura dati immutabile non è possibile modificare un elemento di un vettore già creato ma, invece, è possibile utilizzare il metodo updated per creare un nuovo vettore con un solo elemento modificato: val vettore = Vector(1,2,3,4,5) val vettore2 = vettore updated(4,0) Tramite quest’operazione abbiamo creato un nuovo vettore contenente i seguenti valori 1,2,3,4,0 senza modificare il vettore originale. 27 3.4.4 Collezioni parallele Le collezioni parallele sono state introdotte in Scala nel tentativo di facilitare la programmazione parallela evitando agli sviluppatori di conoscere i dettagli di basso livello della parallelizzazione e nel frattempo fornendo loro un’astrazione di alto livello semplice e familiare. È chiaro, quindi, che l’uso di tale struttura dati è quello di sfruttare, dove possibile, le capacità multi-threading delle moderne architetture hardware. Scala ricorsivamente suddivide la collezione in tante partizioni applicando in parallelo ad ogni partizione un’operazione ed infine unisce tutti i risultati delle operazioni effettuate. La creazione di una collezione parallela avviene utilizzando la seguente sintassi con l’obbligo dell’importazione della classe ParVector: import scala.collection.parallel.immutable.ParVector val p1 = ParVector(1, 2, 3, 4, 5, 6, 7, 8) 3.4.5 Mappe Le mappe sono strutture dati utili per salvare un insieme di coppie chiave/valore. Le chiavi sono uniche all’interno della mappa e ogni valore può essere recuperato in base alla sua chiave. Esistono due tipi di mappe: immutabili e mutabili e differiscono semplicemente perché le mappe immutabili non possono essere modificate una volta create. La sintassi per poter creare una mappa è la seguente: val provincia = Map( “Campania” -> List(“Napoli”, “Benevento”, “Avellino”, “Salerno”, “Caserta”), “Lazio” -> List(“Roma”, “Latina”, “Frosinone”, “Rieti”, “Viterbo”), “Valle d’Aosta” -> List(“Aosta”) ) Scala istanzia di default mappe immutabili e affinché sia possibile creare mappe mutabili è necessario importare la classe “scala.collection.mutable.Map”. Se si ha la necessità di utilizzarle entrambe si possono creare mappe immutabili tramite la parola chiave Map mentre per quelle mutabili la parola chiave mutable.Map. 28 Le operazioni base che fornisce tale struttura dati sono: keys: ritorna tutte le chiavi della mappa. values: ritorna tutti i valori della mappa. 3.5 Cenni sulla programmazione concorrente Con la diffusione sempre maggiore di processori multi-core è diventato indispensabile utilizzare un tipo di programmazione orientato alla gestione della concorrenza. Sin dalle prime versioni Scala ha adoperato il modello degli attori come soluzione alla programmazione concorrente che si basa principalmente sullo scambio di messaggi. Dalla versione 2.11 il sistema interno adoperato da Scala è stato sostituito dal toolkit Akka. Tale modello prevede un alto livello di astrazione semplificando il lavoro dello sviluppatore in quanto non deve più gestire in modo esplicito thread e lock. Ogni attore possiede delle variabili che riflettono lo stato in cui si trova e rappresentano dati importanti che non devono essere corrotti dall’interazione con altri attori. Per questo motivo, a protezione di tali stati, Akka prevede che ogni attore abbia un proprio thread, completamente schermato dal resto del sistema, in modo tale che non bisogna preoccuparsi di sincronizzare l’accesso tramite lock. Ogni volta che un attore riceve un messaggio decide se processare il messaggio, nel caso in cui il client è autorizzato, oppure ignorarlo in caso contrario. Affinché sia possibile gestire la coda messaggi ogni attore possiede una mailbox che può essere implementata con logica FIFO (impostazione di default) oppure con logica di priorità. Ogni attore può creare degli attori-figli a cui può delegare dei sotto-task. La creazione ed eliminazione degli attori-figli avviene in modo asincrono in modo da non bloccare l’esecuzione dell’attore-padre. Quando un attore crea un attore-figlio automaticamente diventa un suo supervisore e in quanto tale ha il compito di gestire gli errori che vengono rilevati dall’attore-figlio scegliendo una tra le seguenti opzioni: Riprendere l’attore-figlio conservando il suo stato interno. Riavviare l’attore-figlio cancellando il suo stato interno. Arrestare definitivamente l’attore-figlio. Infine quando un attore completa le sue operazioni oppure viene terminato libera tutte le 29 sue risorse e le lettere non processate all’interno della mailbox vengono inviate all’EventStream come DeadLetters. Tali lettere sono utilizzate per scoprire le cause di un eventuale crash di sistema. 30 Appendice: Esempi di codice Client – Server Codice relativo al server che apre una connessione sulla porta 19999, salva attraverso un buffer il messaggio ricevuto dal client e lo stampa. Se il messaggio ricevuto dal client contiene la parola “Disconnetti” la connessione è terminata. import java.net._ import java.io._ import scala.io._ object Server extends App { try { val server = new ServerSocket(19999) println("Server inizializzato:") val client = server.accept val in = new BufferedReader(new InputStreamReader(client.getInputStream)).readLine val out = new PrintStream(client.getOutputStream) println("Server ricevuto:" + in) out.println("Messaggio ricevuto") out.flush if (in.equals("Disconnetti")) client.close; server.close; println("Il Server sta terminando la connessione:") } catch { case e: Exception => println(e.getStackTrace); System.exit(1) } } 31 Invece di seguito è mostrato il codice del client nel quale viene creata una finestra grafica di dimensione 500x500 contenente due bottoni: Invia che serve per inviare un messaggio al server con la parola “Hello” mentre Disconnetti termina la connessione. import import import import import java.net._ java.io._ scala.io._ swing._ Swing._ object MyClient extends MainFrame with App { title = "Client" preferredSize = (500, 500) val socket = new Socket(InetAddress.getByName("localhost"), 19999) var in = new BufferedSource(socket.getInputStream).getLines val out = new PrintStream(socket.getOutputStream) println("Client inizializzato:") contents = new BorderPanel { add(new FlowPanel { contents += new Button(new Action("Invia") { def apply { out.println("Hello!") out.flush println("Client received: " + in.next) } }) contents += new Button(new Action("Disconnetti") { def apply { out.println("Disconnetti") out.flush socket.close } }) }, BorderPanel.Position.Center) } pack visible = true } 32 Conteggio parole Di seguito è mostrato il codice di un semplice programma che conta il numero di parole che si trovano all’interno di una frase definito ed eseguito tramite shell interattiva. scala> import scala.collection.mutable import scala.collection.mutable scala> def countWords(text: String) = { | val counts = mutable.Map.empty[String, Int] | for (rawWord <- text.split("[ ,!.]+")) { | val word = rawWord.toLowerCase | val oldCount = | if (counts.contains(word)) counts(word) | else 0 | counts += (word -> (oldCount + 1)) | } | counts | } countWords: (String)scala.collection.mutable.Map[String,Int] scala> countWords("See Spot Run! Run, Spot. Run!") res30: scala.collection.mutable.Map[String,Int] = Map(see -> 1, run -> 3, spot -> 2) 33 Conclusioni Attraverso l’elaborato è stato possibile conoscere l’evoluzione e, soprattutto, le caratteristiche principali che offrono i linguaggi funzionali, garantendo una migliore gestione della modularità rispetto ai linguaggi imperativi. Successivamente è stata fatta un’analisi dettagliata di quali sono la sintassi e le strutture che costituiscono Scala. Come è già stato notato durante l’elaborato, Scala si presenta come un linguaggio a paradigma misto offrendo, quindi, agli sviluppatori le caratteristiche e i vantaggi sia della programmazione ad oggetti che di quella funzionale. In questo modo, amalgamando le varie tecniche, garantisce un maggiore aiuto nella risoluzione di problemi che si possono incontrare. Grazie alle caratteristiche della programmazione funzionale è possibile migliorare la stabilità del software e ridurre i problemi che nascono da effetti collaterali indesiderati. Utilizzando strutture dati immutabili rispetto a quelle mutabili e scegliendo funzioni pure che non interferiscono dannosamente con l’ambiente di sviluppo è possibile scrivere codice molto più sicuro, affidabile e facile da comprendere. Caratteristica da non sottovalutare è l’esecuzione di Scala sulla JVM garantendo massima compatibilità con la maggior parte dei dispositivi che si trovano in commercio. Inoltre permette di importare nei propri progetti tutte le librerie e plugin sviluppati appositamente per Java offrendo, in questo modo, le stesse potenzialità di Java ma ampliandole con le caratteristiche uniche di Scala. Un altro vantaggio molto apprezzato e utilizzato dagli sviluppatori, soprattutto lato server, è la gestione ottimale della concorrenza grazie al modello degli attori di cui è stato fatto un cenno nell’elaborato. Le note negative che si possono riscontrare in tale linguaggio sono da ricercare in un IDE non ancora molto performante nel caso di software che hanno molte dipendenze che interagiscono tra loro e, infine, in una curva 34 d’apprendimento un po’ lenta a causa della complessità dovuta all’insieme di tecniche avanzate appartenenti a due diversi tipologie di linguaggi. 35 Bibliografia [1] Martin Odersky, Lex Spoon, and Bill Venners, Programming in Scala, 2008. [2] John Hughes, Why Functional Programming Matters, 1990. [3] Alvin Alexander, Scala Cookbook, 2013. [4] Scala Documentation, http://www.scala-lang.org/documentation/, 4/09/16 [5] Tutorials Point, http://www.tutorialspoint.com/scala/scala_overview.htm, 01/09/16. [6] Haskell Wiki, https://wiki.haskell.org/Functional_programming, 28/07/16 [7] Wikipedia, Scala Programming Language, https://en.wikipedia.org/wiki/Scala_(programming_language), 2/09/16 [8] Wikipedia Italia, Scala (linguaggio di programmazione), https://it.wikipedia.org/wiki/Scala_(linguaggio_di_programmazione), 2/09/16 [9] Akka Documentation, http://doc.akka.io/docs/akka/2.4/scala.html, 3/09/16 [10] Wikipedia, Akka (toolkit), https://en.wikipedia.org/wiki/Akka_(toolkit), 3/09/16 [11] History of Functional Languages, http://caml.inria.fr/pub/docs/fpcl/fpcl-02.pdf, 25/07/16 [12] Courseware on Functional Programming, http://athena.ecs.csus.edu/~csc135fp/project/history.html, 25/07/16 36