Elaborato Casillo Davide N46001379

annuncio pubblicitario
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
Scarica