SCALA
Scala e uno degli esempi più fiorenti di linguaggio blended, riprende molte caratteristiche dei linguaggi funzionali e possiede inoltre un type system forte
CARATTERISTICHE
Scala consente di definire in un inner scope una variabile omonima a una già esistente in un outer scope (shadowing)
FUNZIONI
le funzioni sono introdotte dalla parola chiave def
e specifica di tipo postfissa (anziché prefissa come in C e Java), e solitamente omessa se può essere inferita
def abs( x:Float ) : Float={
if (x<0) -x else x
}
println(abs(-4))
TYPE INFERENCE
La specifica di tipo può essere omessa in tutti quei casi in cui possa essere inferita in modo certo
//Specifica di tipo OMESSA: si può dedurre un tipo di risultato Float
def abs( x:Float ) = {
if (x<0) -x else x
}
Una funzione che non restituisce nulla ha tipo di ritorno Unit
def hello( s:String ) = {
println("Hello" + s)
}
hello("world")
scala volutamente non supporta l’assegnamento multiplo a tal fine, l’assegnamento è una espressione di tipo Unit che restituisce la speciale costante ()
STRUTTURE DI CONTROLLO
Scala ha solo cinque strutture di controllo predefinite: if
, while
, for
, try
, match
Questo per evitare il proliferare di costrutti, inoltre scala fornisce all’utente la possibilità di definire nuovi costrutti, in modo da arricchire il linguaggio per mezzo di nuovi costrutti anziché librerie, inoltre tutti i costrutti sono espressioni in modo da favorire approcci più funzionali
PROGRAMMA STANDALONE
In Scala, il main non può stare in una classe, che non ammette membri statici: sta in un oggetto singleton (più OOP)
object Test1 {
def main(args: Array[String]) : Unit = {
var k=0;
while (k<5) {
println("Iterazione " + k)
k += 1; // l'operatore ++ non esiste
}
}
}
NOTAZIONE INFISSA
scala consente di richiamare un dato operatore per mezzo della notazione infissa
//equivalente a scrivere Array.apply("Paperino", "Pippo", "Pluto")
val v = Array("Paperino", "Pippo", "Pluto");
//equivalente a scrivere 0.to(2)
for (i <- 0 to 2) println(v(i))
Dualmente, una lista di valori fra parentesi usata come L-value in un assegnamento sottintende una chiamata implicita al metodo update dell’oggetto ricevente ergo:
val v = Array("Paperino", "Pippo", "Pluto");
v(2) = "Quack!"
// equivale a:
v.update(2,"Quack!")
println(v(2))
PROCEDURE
Una funzione che non restituisce nulla è di tipo Unit
, (non void
),gli argomenti sono sempre implicitamente val
, non var
def change(i:Int) : Unit = { i = 2 }
esplode perché si sta cercando di modificare un
val
una funzione priva di un’istruzione return
esplicita restituisce comunque l’ultimo valore calcolato
def increment(i:Int) : Int = i+2
println(increment(3))
Scala non consente di definire funzioni con un numero variabile di argomenti ma permette di ripetere l’ultimo argomento, con una sintassi analoga alle regular expression: *
def myprint(args: String*)
Attenzione ai tipi degli argomenti ripetuti: internamente alla funzione, l’argomento ripetuto è mappato su un array, quindi il tipo dell’argomento ripetuto è
Array
di quel tipo esternamente, però, il tipo argomento ripetuto è considerato diverso da un array e quindi incompatibile con esso
OGGETTO SINGLETON
scala consente la definizione di oggetti singleton introdotti dalla parola chiave object
anziché class
, sono strutturalmente identici a una classe ma non definiscono un tipo perché non ne esisteranno altre istanze
object Boss {
//Tutti i campi devono essere obbligatoriamente inizializzati
private val name = "The Boss";
//Proprietà e metodi di un singleton sono pubblici per default
def getName() = name
}
object Main {
def main(args: Array[String]) {
//L'oggetto singleton è usabile senza doverlo creare esplicitamente
println(Boss.getName())
}
}
Un oggetto singleton può essere usato:
- da solo, come standalone object
- insieme a una classe, come suo companion object
- nella composizione di tratti
COMPANION OBJECT
Un oggetto companion è un particolare singleton che:
- e omonimo di una classe
- e definito nel suo stesso file
class Counter(v:Int) {
//Accesso privilegiato al campo privato dell'oggetto companion
Counter.howMany += 1;
}
object Counter {
//Variabile privata dell’oggetto singleton
private var howMany : Int = 0
}
COSTRUTTORI
In Scala una classe non ha costruttori espliciti al loro posto, ha parametri di classe usati dal compilatore per generare automaticamente il costruttore primario, inoltre per ridefinire metodi ereditati e necessario il qualificatore di override
class Counter(v:Int) {
private var value = v;
def setValue(v:Int) = { value = v }
def getValue() : Int= value
//Ridefinizione di metodo
override def toString() = "Counter di valore " + value
}
FUNZIONI E CHIUSURE
Il pieno supporto alle funzioni come first-class objects comporta un altrettanto pieno supporto alle chiusure, per cui viene preferita la chiusura lessicale e le variabili libere vengono chiuse per mezzo di riferimenti alle variabili esterne, ciò significa che risentono dei cambiamenti ad esse
CONVERSIONI IMPLICITE
Come in C++, scala consente di definire conversioni implicite di tipo che saranno applicate automaticamente, questo torna utile per permettere l’uso commutativo degli operatori
class Frazione(val num:Int,val den:Int){
// lancia IllegalArgumentException se violata
require(den!=0)
//costruttore ausiliario
def this(n:Int) = this(n,1)
// tail recursion ottimizzata
def calcMCD(a:Int, b:Int) : Int = if (a%b==0) b else calcMCD(b,a%b)
//operatore + frazione
def +(i:Int): Frazione = sum(new Frazione(i))
def sum(f: Frazione): Frazione = {
val n = this.num * f.den + this.den * f.num;
val d = this.den * f.den;
//tonde opzionali, principio di accesso uniforme
return new Frazione(n,d).minTerm;
}
def minTerm : Frazione = {
val mcd = calcMCD(num.abs, den.abs);
val nuovoNum = num/mcd;
val nuovoDen = den/mcd;
new Frazione(nuovoNum, nuovoDen);
}
}
Ciò consente di scrivere espressioni come println(f1+3)
MA NON 3+f1
perché +
è un metodo di Frazione
, non di Int
class Frazione(val num:Int,val den:Int){
// lancia IllegalArgumentException se violata
require(den!=0)
//costruttore ausiliario
def this(n:Int) = this(n,1)
// tail recursion ottimizzata
def calcMCD(a:Int, b:Int) : Int = if (a%b==0) b else calcMCD(b,a%b)
//operatore + frazione
def +(i:Int): Frazione = sum(new Frazione(i))
def sum(f: Frazione): Frazione = {
val n = this.num * f.den + this.den * f.num;
val d = this.den * f.den;
//tonde opzionali, principio di accesso uniforme
return new Frazione(n,d).minTerm;
}
def minTerm : Frazione = {
val mcd = calcMCD(num.abs, den.abs);
val nuovoNum = num/mcd;
val nuovoDen = den/mcd;
new Frazione(nuovoNum, nuovoDen);
}
override def toString = if(den==1) ""+num else num+"/"+den;
}
//companion object con la definizione del metodo di conversione
object Frazione {
def main(args: Array[String]) : Unit = {
val f = new Frazione(3,5)
println(f + 3) // SI', con operator+ overloaded
}
}
ECCEZIONI
Le eccezioni in scala sono simili a java, con alcune differenze tra cui:
- anche l’espressione throw ha un tipo di ritorno:
Nothing
che non sarà mai valutata e quindi non produrrà mai un “valore” - la clausola catch opera per pattern matching
- anche il costrutto
try/catch/finally
è un’espressione che restituisce il valore del ramotry
, se esso ha avuto successo altrimenti restituisce il valore di uno dei ramicatch
, se l’eccezione è stata catturata e non restituisce alcun valore, se l’eccezione è stata lanciata ma non catturata
a differenza di Java, l'eventuale valore computato nel finally non è considerato e va quindi perduto.
object TestExceptions {
def main(args: Array[String]) : Unit = {
val myurl = getURL("https://carnivuth.github.io/linguaggi_modelli_computazionali/")
val bleah = getURL("carnivuth.github.io/linguaggi_modelli_computazionali/")
println(myurl)
}
def getURL(url: String) = {
try {
new java.net.URL(url)
} catch {
case e: java.net.MalformedURLException => println("bleah")
}finally println("done.")
}
}
ogni
case
cattura un caso attraverso pattern matching
PATTERN MATCHING
In scala e possibile “smontare” un oggetto (object destructuring) in un insieme di componenti che possono poi essere riferite successivamente
val coppia = (12, "dozzina")
val (value,name) = coppia
println(value)
println(name)
Questo torna molto utile insieme al for
per accedere in maniera pulita alle componenti di un oggetto
val persone=(("mario","rossi"),("mario","rossi"))
for ((name,surname) <- persone) print(surname+" "+name)
COSTRUTTO MATCH
scala rimpiazza il costrutto switch con un ben più potente costrutto match
con le seguenti caratteristiche:
- semantica a casi distinti, senza “fall through” (niente break)
- nel costrutto
case
non solo sono ammesse etichette scalari o stringhe, ma anche selettori evoluti sfruttando il pattern matching su variabili o costruttori di istanze
case class Point(val x:Int,val y:Int){}
object Point{
def main(args:Array[String]): Unit ={
var arg:AnyRef=Point(3,5)
println(
arg match{
case i:Integer => "Intero";
// con la possibilita di destrutturare l'oggetto
case Point(x,y) => "punto "+x+","+y;
case o:Any => "oggetto";
}
)
}
}
DEFINIRE UN COSTRUTTO BUILTIN
Scala ha tutti gli strumenti per consentire la definizione di costrutti builtin:
- block like syntax
- currying
- call by name
- chiusura
quindi per creare il costrutto repeat
:
// si sfrutta il curring per definire una funzione a 2 argomenti
def repeat(n:Int)(expr: =>Unit):Unit={
//tail recursion per ripetere l'azione dove si richiama la funzione esterna della chiusura
expr; if (n>1) repeat(n-1)(expr)
}
//il chiamante puo sfruttare la block like sintax e il costrutto e completato
repeat(3){print("hi")}
EREDITARIETÀ ESTESA: I TRATTI
Scala estende il concetto delle interfacce java con i tratti, un tratto si definisce come segue
trait tratto{}
Come per le interfacce java un tratto non può generare istanze, ma al contrario delle stesse può definire codice e estendere altre classi
MIXING DEI TRATTI
un tratto può ereditare sia da un altro tratto che da una classe e una stessa classe può comporsi di più tratti:
class Animale(){}
trait respira extends Animale{}
trait cammina extends Animale{}
class Cane extends Animale with respira with cammina(){}
In questo caso essendo che i tratti estendono direttamente la classe
Animale
possono essere composti solo con classi e tratti che estendono la stessa classe
I tratti possono essere composti anche in fase di istanziazione di un oggetto:
class Animale(){}
trait respira extends Animale{}
trait cammina extends Animale{}
val a = new Animale with cammina()
val b = new Animale with respira()
TRATTI E OVERRIDE DI METODI
Un tratto puo a sua volta effettuare l’override di metodi della classe da cui eredita, per evitare ambiguita in caso di tratti che estendano lo stesso metodo, la risoluzione della keyword super
viene effettuata a runtime
class Animale(nome:String){
override def toString():String={return this.nome}
}
trait Respira extends Animale{
override def toString():String={return super.toString + " respira"}
}
trait Cammina extends Animale{
override def toString():String={return super.toString + " cammina"}
}
println((new Animale("asdrubale") with Cammina with Respira).toString())
println((new Animale("ubaldo") with Respira with Cammina).toString())
RISOLUZIONE DI super
PER MEZZO DELLA LINEARIZZAZIONE
e possibile comporre i tratti perché la risoluzione delle catene di super
avviene a runtime sfruttando il fatto che i tratti non possono avere costruttori di conseguenza non ci sono ambiguità nella catena risolutiva
flowchart TD A[calls Respira.toString] B[calls Cammina.toString] C[calls Animale.toString] A --> B --> C
ALGORITMO DI LINEARIZZAZIONE
data una classe definita come segue
class A extends B with C with D //.....
L’algoritmo di linearizzazione procede come segue:
- si considera al catena della classe padre
B
(che e sempre e solo una) - si aggiungono se non presenti le catene dei tratti (da sinistra a destra)
COLLECTIONS
In scala le collection sono fornite in due versioni, (per non scoraggiare troppo le migrazioni di codebase da java)
- mutable (necessario import esplicito
scala.collection.mutable
) - immutable (importate di default)
tra le collection sono disponibili List
Array
Tuple
Set
Map
VARIANZA E COVARIANZA
In scala e possibile specificare la varianza dei tipi parametrici
class Coda[+T](...) {
def append[U >: T](element: U) {..}
}
TIPO Option
Scala introduce il tipo Option
come analogo degli optional di java, questo per evitare argomenti passati come null (si ricorda che ciò in scala non e possibile per la gerarchia di tipi)
EXTRACTORS
Le funzionalità di pattern matching di scala come definite sopra sono utilizzabili solo per mezzo di classi dati, per poterle utilizzare anche con oggetti generati da classi normali scala mette a disposizione gli extractors, ovvero oggetti che definiscono un metodo unapply()
duale di apply()
che estrae i valori di un oggetto
Il metodo in questione deve essere definito al difuori della classe interessata (il miglior candidato risulta essere il companion object)
// oggetto companion
object Persona {
def unapply(p:Persona) : Option[(String,Int)] =Some(p.nome,p.year)
}
// utilizzo dell extractor
def filtra(x : AnyRef) =
x match {
case Persona(n, a) => a
case _ => "non mi piace"
}
INTEROPERABILITÀ CON JAVA
Scala, compilando in bytecode e eseguendo sulla JVM e direttamente interoperabile con java, infatti alcune delle funzionalita introdotte da scala sono mappate quasi in corrispettive astrazioni java
- gli oggetti singleton vengono mappati su classi con metodi statici
- i tratti implementati come una interfaccia con una classe accessoria per il corpo dei metodi
- i tipi generici si mappano quasi interamente sui tipi generici di java
SCALA | JAVA |
---|---|
usare classi java | usare classi scala |
estendere classi java | usare oggetti scala (con delle limitazioni) |
usare framework java javaFX | usare classi scala con funzioni e chiusure |
INTERPRETI IN SCALA: LA LIBRERIA PARSER COMBINATORS
La libreria scala parser combinators
consente la creazione di parser custom per grammatiche anche non deterministiche (a scapito delle performance).
La libreria definisce i seguenti operatori:
|
per esprimere l’alternativarep()
per esprimere la ripetizione di un elemento~
per esprimere la concatenazione()
per raggruppare
La grammatica viene definita estendendo la classe java JavaTokenParser
e le regole di produzione vengono mappate su funzioni scala
class MyGrammar extends JavaTokenParsers {
def A: Parser[Any] = A~rep("+"~B | "-"~B)
def B: Parser[Any] = B~rep("*"~B | "/"~B)
def C: Parser[Any] = floatingPointNumber | "("~A~")"
}
la semantica può essere aggiunta con l’operatore ^^
class MyGrammar extends JavaTokenParsers {
def A: Parser[Any] = A~rep("+"~B | "-"~B)^^ {
// funzione che implementa la semantica
println(a)
}
}
In caso di grammatiche ll(1) la libreria genera comunque un parser in grado di fare backtracking, per evitare l’inefficienza e utile specificare che non e necessario supportarlo con l’operatore !