Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 1

Università degli Studi di Bologna
Scuola di Ingegneria e Architettura
Capitolo 24
Scala: argomenti avanzati
INTEROPERABILITÀ
SCALA / JAVA
A blend of functional and object-oriented on the JVM
Corso di Laurea Magistrale in Ingegneria Informatica
Anno accademico 2014/2015
Prof. ENRICO DENTI
Dipartimento di Informatica – Scienza e Ingegneria (DISI)
INTEROPERABILITÀ SCALA / JAVA
• Scala è compilato in bytecode, quindi tipicamente
l'interoperabilità fra i due è straightforward
• molte cose restano identiche (oggetti, metodi, stringhe, eccezioni..)
• ogni tipo Java ha un equivalente Scala, che così può accedere a ogni classe Java
• le eccezioni però in Scala non sono checked: no problem.. la JVM non controlla!
• altre cose si mappano 1-1 quasi sempre (valori primitivi)
• Alcune novità però richiedono un mapping ad hoc
• oggetti singleton classe con metodi statici (circa..)
• viene creata una classe ad hoc, chiamata nome$, con un campo statico
final di nome MODULE$ che contiene il riferimento all'istanza singleton
• tratti interfaccia + classe accessoria (circa..)
• caso particolare: tratto con soli metodi astratti solo interfaccia
• annotazioni doppio processing (Scala, poi Java) JUnit OK
• tipi generici molto simili, ma con dettagli diversi…
INTEROPERABILITÀ: ESEMPIO
Data la seguente classe Java che modella uno Studente:
public class Studente {
String nome;
Collection<String> esami;
public Studente(String nome, Collection<String> esami) {
this.nome=nome; this.esami=esami;
}
public Studente(String nome){
this(nome, new ArrayList<String>());
}
public Collection<String> getEsami(){ return esami; }
public String getNome(){ return nome; }
@Override public String toString() {
return "Studente "+ getNome() + " - Esami " + getEsami();
}
}
INTEROPERABILITÀ SCALA / JAVA
• Da Scala si possono:
• usare classi Java così some sono
• scrivere classi Scala che estendano classi Java
• istanziare classi Java (ottenendo oggetti Java/Scala validi)
• accedere metodi o campi-dati, anche statici
• sfruttare framework come Swing "in salsa Scala"
• gestione semplificata degli eventi (meno "boilerplate code")
• derivando da scala.swing.SimpleGUIApplication
• Da Java si possono:
• usare classi Scala così some sono
• usare oggetti Scala (con un po' di attenzione..)
• usare classi Scala che rappresentino funzioni e chiusure
• …e in generale fare "quasi tutto" ☺
INTEROPERABILITÀ: ESEMPIO
Per istanziare uno Studente, in Java scriveremmo:
public static void main(String[] a){
Studente s1 = new Studente("John");
Boilerplate code
Studente s1 = new Studente("Jeff",
Arrays.asList(new String[]{"Analisi", "Fisica"}) ));
System.out.println(s1);
System.out.println(s2);
}
Per farlo dall'interprete Scala basta scrivere:
conversioni automatiche
liste Java liste Scala
Boilerplate code SCOMPARSO
purché la classe Java sia nel classpath corrente.
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
1
USO DI JAVA DALL'INTERPRETE SCALA
• L'interprete Scala permette di usare classi Java semplicemente "nominandole", senza caricamenti espliciti,
purché siano reperibili nel classpath
• Il classpath si può estendere col comando :cp
USO DI JAVA DALL'INTERPRETE SCALA
AVVERTENZA
• l'interprete carica le classi una sola volta, al primo uso
• ergo, se una classe viene modificata o ricompilata
successivamente, lui non se ne accorge
• che però ha problemi con le specifiche di unità ("E:\...")
• In alternativa, si può importare una classe Java con
l'istruzione Scala import
• questo comportamento va tenuto presente: altrimenti, si
otterranno messaggi d'errore incomprensibili..
• Per ovviare:
• chiudere e riaprire l'interprete ☺
• oppure, usare (con accortezza..) il comando :replay che
resetta lo stato e ripete tutti i comandi successful precedenti
(e quindi ricarica anche la classe…)
• non è un comando dell'interprete (non ne ha neanche la sintassi!),
è un'istruzione Scala (la stessa che scrivereste in un programma)
• come tale, trova le classi meglio dell'interprete ☺
• Quindi, l'esempio precedente funziona solo se la classe
Studente è già nel classpath o viene importata.
CONVERSIONI AUTOMATICHE
DI STRUTTURE DATI JAVA / SCALA
• Nonostante i nomi analoghi, le classi-collection Scala
non sono le stesse definite da Java
CONVERSIONI AUTOMATICHE
DI STRUTTURE DATI JAVA / SCALA
• Altre conversioni invece funzionano in un solo verso:
Nel senso opposto:
UnsupportedOperationException
• sono a tutti gli effetti TIPI DIVERSI (nome assoluto diverso)
• che, quindi, non possono essere passati uno al posto dell'altro
• L'oggetto collections.JavaConversions fornisce
una serie di conversioni implicite pronte all'uso
CONVERSIONI AUTOMATICHE
DI STRUTTURE DATI JAVA / SCALA
•
Il motivo per cui non operano anche "a rovescio" è che Scala definisce
quelle classi in doppia versione (mutevole e immutabile) ambiguità
•
Ricordare che Scala non importa di default le versioni mutevoli, quindi,
volendole usare, occorrerà importarle esplicitamente.
•
UNICO NEO: come tutte le conversioni implicite, anche queste
potrebbero scattare inaspettatamente.
•
ALTERNATIVA: l'oggetto JavaConverters offre le stesse
funzionalità con metodi espliciti (asJava, asScala)
INTEROPERABILITÀ SCALA / JAVA:
UN PROBLEMA DI.. TIPI
• Problema: Java prevede wildcard e tipi "raw", Scala no
Esempio con JavaConversions
importa le versioni mutevoli
ArrayBuffer Scala
in List Java…
… e ritorno, da List
Java a Seq Scala
• Wildcard: Iterator<?>,
• Tipi raw:
Iterator<? extends T>…
Iterator (senza specifica ulteriore Object)
• Per gestirli, Scala definisce i tipi esistenziali
• una caratteristica generale del linguaggio (keyword forSome),
usata però praticamente solo per accedere a tipi Java da Scala
• SINTASSI:
tipo forSome { dichiarazioni }
• ESEMPI:
Iterator[T] forSome { type T }
Iterator[T] forSome { type T <: U }
• SHORTCUT:
la prima si riassume in Iterator[_]
la seconda si riassume in Iterator[_<: U]
(placeholder syntax per tipi esistenziali)
dove dichiarazioni è un elenco di val e type
Idem con mappe:
una mappa Scala
(sopra) convertita in
Map Java (sotto)
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
2
TIPI ESISTENZIALI: MOTIVAZIONE
• A cosa servono?
TIPI ESISTENZIALI: NAMING ISSUE
• Ma… il tipo esistenziale come si chiama?
• a dare un "corrispondente Scala" ai tipi Java con wildcard
• NON SI CHIAMA, perché non ha nome
• quindi è impossibile referenziarlo direttamente
Esempio: la classe Java:
public class ExType {
public Collection<?> getContent(){
Collection<Integer> c = new ArrayList<Integer>();
c.add(1); c.ad(23); c.add(456); ...
return c;
}
}
può essere acceduta da Scala direttamente, scrivendo:
Esempio: data la classe Java precedente.. come definire il
tipo di un set Scala cui mettere i risultati?
val myIter = new ExType().getContent.iterator
val mySet = scala.collection.mutable.Set.empty[???]
while (myIter.hasMore) mySet += myIter.next()
Il problema è che getContent restituisce una Collection<?> e quindi il
tipo degli elementi del set è ignoto.
Questo caso non può essere gestito, va riformulato.
sintassi shortcut per tipo esistenziale
TIPI ESISTENZIALI: NAMING ISSUE
Possibile riformulazione:
• evitare di restituire un tipo esistenziale
• al suo posto, restituire un oggetto con membri astratti e idonei metodi
• spostare gli argomenti-tipo nei parametri dei metodi
CASE CLASSES
& PATTERN MATCHING
In pratica: si predispone una classe extra con membri astratti
abstract class Accessory {
type E; val set: scala.collection.mutable.Set[E]
}
.. e si riformula opportunamente il metodo usando quella:
def azione[T]( c: Collection[T] ) : Accessory = {
val mySet = scala.collection.mutable.Set.empty[T]
while (myIter.hasMore) mySet += myIter.next()
return new Accessory {type E = T; val set = mySet }
Ora il tipo è giustamente nominabile ☺
}
CASE CLASSES
• Scala introduce case classes come modo compatto per
esprimere oggetti su cui fare pattern matching
• Una case class
• SINTATTICAMENTE, è introdotta dal modificatore case
• SEMANTICAMENTE, è gestita in modo speciale dal compilatore
• Il compilatore aggiunge metodi e qualifiche:
• un metodo factory con lo stesso nome della classe
• implementazioni "naturali" per toString, equals e hashcode
• tutti gli argomenti sono implicitamente val e perciò mantenuti
come campi-dati (non solo come parametri di classe)
• RISULTATO: alta espressività, poco "boilerplate code"
ESEMPIO 40: CASE CLASS
Una gerarchia di case class per le espressioni
Classe base astratta:
abstract class Expr
Sottoclassi in forma di case class:
case
case
case
case
class
class
class
class
Number(v:Double) extends Expr;
Variable(s:String) extends Expr;
UnaryExp(s:String, arg:Expr) extends Expr;
BinaryExp(arg1:Expr, s:String, arg2:Expr)
extends Expr;
Essendo case class invece che "semplici" class, possono
ora essere usate nel pattern matching, oltre che per creare
normalmente oggetti.
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
3
PATTERN MATCHING
• Le case classes sono il presupposto naturale su cui
Scala definisce il meccanismo di pattern matching
• NUOVA SINTASSI: espressione match
selector match { alternatives }
dove le varie alternatives hanno ciascuna la forma
case pattern => expressions
• Match e switch a confronto
• il costutto match è la versione evoluta dell'antico switch
• è più semplice sintatticamente (non occorrono le parentesi tonde
attorno al selettore, la sintassi è infissa) ma è molto più potente
• perché i pattern non sono necessariamente costanti: possono
essere anche variabili o constructor pattern (o una wildcard)
ESEMPIO 40: PATTERN MATCHING
PATTERN MATCHING
• Poiché match è un'espressione, denota un valore
• per questo, se nulla fa match coi casi indicati, viene lanciata
un'eccezione di tipo MatchException
• Un pattern può avere la forma di:
• costante fa match con esattamente quel valore (come switch)
• variabile fa match con qualunque valore (e lo cattura)
• wildcard fa match con qualunque valore (e lo ignora)
• costruttore fa match con qualunque valore della case class
i cui argomenti facciano a loro volta match
pattern matching ricorsivo
• Convenzione:
le costanti iniziano per Maiuscola, le variabili per minuscola
(nel caso servano costanti minuscole, usare i backtick)
ESEMPIO 40: PATTERN MATCHING
Sfruttando le case class, è facile scrivere funzioni che
intercettino situazioni e agiscano di conseguenza.
Ad esempio, per semplificare un'espressione:
def semplifica(e : Expr) =
Non serve la new
e match {
case UnaryExp("-", UnaryExp("-",e)) => e;
case BinaryExp(e, "+", Number(0))
=> e;
case BinaryExp(e, "*", Number(1))
=> e;
case _ => e;
Non serve la new
}
- ( -e ) = e
e+0 = e
e*1 = e
Vantaggi:
• diventa facile delegare al linguaggio sia la selezione della situazione
(best match), sia i corrispondenti mapping delle variabili
• sintassi leggera: non occorre la new per creare istanze
ESEMPIO 40 (revised) – CON PACKAGE
ESEMPIO 41: PATTERN MATCHING
Estendiamo l'esempio per catturare altri pattern
OSSERVA: uscendo dallo
scope delle parentesi graffe, si
torna automaticamente al
namespace esterno (default)
def semplifica(e : Expr) =
Si può togliere il "-"
e match {
esterno cambiando
case UnaryExp("-", UnaryExp("-",e)) => e;
segno al valore interno
case UnaryExp("-", Number(v)) => Number(-v);
case UnaryExp("+", Number(v)) => Number(v);
case BinaryExp(e, "+", Number(0))
=> e;
Si può togliere il "+"
case BinaryExp(e, "*", Number(1))
=> e;
esterno senza far niente
case _ => e;
perché è inutile
}
• Il pattern matching cattura il valore della variabile v
• Tale valore è disponibile per costruire nuove istanze di Number
In questo modo è più semplice tenere più versioni del file nello stesso
progetto (senza package, le classi risulterebbero duplicate)
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
4
ESEMPIO 41: PATTERN MATCHING
ESEMPIO 41 – CON PACKAGE
Un possibile miglioramento
def semplifica(e : Expr) =
e match {
case UnaryExp("-", UnaryExp("-",e)) => e;
case UnaryExp("-", Number(v)) => Number(-v);
case UnaryExp("+", Number(v)) => Number(v);
case BinaryExp(e, "+", Number(0))
=> e;
case BinaryExp(e, "*", Number(1))
=> e; Number(v) è riusata "as
is" perché ricrearla?
case _ => e;
}
• è inutile ricreare una struttura identica a una già esistente
• se serve "as is", si può catturarla in una variabile di appoggio
con la notazione @
Riusiamo quella che c'è già !
Ora semplifica anche:
–(Number(-3.6)) in Number(3.6)
+(Number(-3.7)) in Number(-3.7)
–(Number(3.9)) in Number(-3.9)
case UnaryExp("+", e @ Number(v)) => e
ESEMPIO 42: PATTERN MATCHING
I pattern possono avere la forma di List, Tuple, etc
def filtra(x : AnyRef)
x match {
case List(1,_,_) =>
case List(2, _*) =>
case (x, y)
=>
case _
=>
}
ESEMPIO 43: TYPED PATTERN
I pattern possono anche essere tipati, sostituendo così
cast, instanceof e altri test sul tipo di un oggetto.
=
"Lista
"Lista
"Tupla
x + "
di 3, inizia per 1"
che inizia per 2"
di due elementi"
non mi piace"
def filtraPerTipo(x
x match {
case s: String
case y: Map[_,_]
case _
}
Argomenti ripetuti
catturano liste di
qualsiasi lunghezza
(maggiore di 1)
: Any) =
=> operazione sulla stringa s
=> operazione sulla mappa y
=> operazione di default
Ad esempio, concretamente:
println(filtra(
println(filtra(
println(filtra(
println(filtra(
println(filtra(
println(filtra(
println(filtra(
println(filtra(
println(filtra(
println(filtra(
Array(1,2,3) ))
List(1,2,3) ))
List(4,5,6) ))
List(2,2,5) ))
List(2,1,4,5,6,2,5) ))
List(2) ))
(3,2) ))
("ciao","mondo") ))
(3,2,0) ))
("bah") ))
//
//
//
//
//
//
//
//
//
//
nessun filtraggio
intercettata!
nessun filtraggio
intercettata!
intercettata!
intercettata!
intercettata!
intercettata!
nessun filtraggio
nessun filtraggio
[I@1b7ebdf8 non mi piace
Lista di 3, inizia per 1
List(4, 5, 6) non mi piace
Lista che inizia per 2
Lista che inizia per 2
Lista che inizia per 2
Tupla di due elementi
Tupla di due elementi
(3,2,0) non mi piace
bah non mi piace
ESEMPIO 44: TYPED PATTERN
Ad esempio, concretamente:
def checkType(x : Any)
x match {
case y: List[Int]
case v: List[String]
case u: List[Char]
case _
}
=
=>
=>
=>
=>
y(0)
v(1)
u(2)
x
Any) =
s.length
y._1
i
x
idem fra y e x
Alternativa: i due operatori
isInstanceOf e asInstanceOf
Test: x.isInstanceOf[Int]
Cast: x.asInstanceOf[Int]
println(filtraPerTipo(2))
2
println(filtraPerTipo("ciao"))
4
println(filtraPerTipo(('a',3))) a
ESEMPIO 45: PATTERN MATCH SEMANTICO
.. ma attenzione alla type erasure! I tipi interni delle
collection non sono noti a runtime… è tutto Object!
def checkType(x : Any) =
x match {
case y: List[Char] => …
case v: List[Int] => …
case _
=> …
}
def filtraPerTipo(x :
x match {
case s: String
=>
case y: (u,v)
=>
case i: Int
=>
case _
=>
}
s e x sono riferimenti
identici, ma di tipo diverso:
s = (String) x
Se il match sintattico non basta guardie semantiche
• utili per superare i limiti del pattern matching lineare
• espressioni della forma if condizione davanti a =>
Non può verificarlo!
ESEMPIO: intercettare espressioni come x+x semplificandole in x*2
La JVM non mantiene l'informazione di tipo a run time!
•
Warning di compilazione (non-variable type argument is unchecked since
it is eliminated by erasure)
.. quindi scatta sempre il primo (gli
altri sono irraggiungibili – unreachable
code case v: List[String] => v(1))
checkType(List(3,4)
checkType("ciao","mondo)
non si può fare sintatticamente, perché non c'è unificazione
def semplifica(e : Expr) =
NO, perché x non può comparire due volte
e match {
(restrizione al pattern matching lineare)
...
case BinaryExp(x, "+", x) => BinaryExp(x, "*", Number(2));
}
•
ma si può fare semanticamente, con una guardia:
case BinaryExp(x,"+",y) if x==y => BinaryExp(x,"*",Number(2));
3
ciao
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
SI, perché sono due variabili con guardia semantica
5
ESEMPIO 46: PATTERN MATCH SEMANTICO
ATTENZIONE, però: il matching semantico non rileva gli
eventuali casi mancanti
ESEMPIO
dato il costrutto match seguente:
def f(x:Int) = x
case x if x>0
case x if x==0
case x if x<0
}
match {
=> x+4
=> 7
=> -x
ESEMPIO 47: PATTERN DI "SMONTAGGIO"
Un pattern può essere utile per "smontare" un valore
composto nelle sue parti componenti
• anche per definire nuove variabili
ESEMPIO:
•
data la tupla
val coppia = (12, "dozzina")
•
si può sfruttare il pattern matching per "smontare" la tupla e
definire due variabili u, v che ne catturino le due parti:
l'eventuale "scopertura" di un caso non viene rilevata:
def f(x:Int) = x match {
case x if x>0 => x+4
case x if x<0 => -x
}
PATTERN MATCHING
per definire LETTERALI FUNZIONE
• Il costrutto case è più potente di quanto finora visto
• In realtà, una sequenza di case fra graffe costituisce
un function literal
val (u, w) = coppia
•
ottenendo in risposta
ESEMPIO 48: ENTRY POINT MULTIPLI
val azione : (Seq[String]) => Unit = {
case s if s.length==1 => print("ricevuto " + s(0))
case s if s.length==2 => print("invio di " + s(0) +" a " + s(1))
}
– può quindi essere usato ovunque si attenda una funzione
– ma è più generale perché di fatto ha entry point multipli
(ognuno, volendo, con propria lista di argomenti differenziata)
tipo del letterale-funzione
val f:
case
case
case
}
Int
x if
x if
x if
=> Int =
x>0 =>
x==0 =>
x<0 =>
{
x+4
7
-x
Definizioni alternative
tramite case multipli
val action : Any => Unit = {
case s:Int => print("intero " + s)
case List(n) => print("lista " + n)
case a:String => print("msg "+ a)
}
ESEMPIO 49: FUNZIONI PARZIALI
PATTERN MATCHING e FUNZIONI PARZIALI
Letterale-funzione TOTALE
• Il rischio che una serie di case non copra tutti i casi è
reale: se accade esplosione a run time
• Si può controllare il rischio definendo esplicitamente
una funzione parziale
Lista di UN SOLO elemento
val f:
case
case
case
}
– da non confondere con le funzioni parzialmente applicate!!
Int
x if
x if
x if
=> Int =
x>0 =>
x==0 =>
x<0 =>
{
x+4
7
-x
Letterale-funzione PARZIALE
val f:
case
case
case
}
PartialFunction[Int,Int] = {
x if x>0 => x+4
x if x==0 => 7
x if x<0 => -x
Stesso output, perché in
realtà questa funzione è
definita in TUTTI i casi
– una funzione parziale è istanza di PartialFunction
anziché Function
– il compilatore avviserà se ci sono casi non coperti
– il metodo ausiliario isDefinedAt permetterà di verificare a
runtime il rispetto della precondizione
• previene l'esplosione..
val f: PartialFunction[Int,Int] = {
case x if x>0 => x+4
case x if x<0 => -x
}
Se però si toglie il caso x==0
ora è possibile verificarlo:
• …ma al costo di fare due volte lo stesso controllo!
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
6
CLASSI SEALED (SIGILLATE)
PATTERN MATCHING e FOR
• Il costrutto for si sposa particolarmente bene col
pattern matching
– facile iterare "legando variabili" di pattern composti
– in un modo molto simile all'unificazione Prolog ☺
• In varie situazioni è utile impedire che si possano
derivare da una classe altre sottoclassi..
– come da sempre si ottiene col qualificatore final
• …tranne un insieme di classi prestabilito
– in Java questo non si riesce facilmente a fare
– in Scala si ottiene col qualificatore sealed
val capitali = Map(
"Francia" -> "Parigi",
"Germania" -> "Berlino",
"Italia" -> "Roma")
Pattern matching
A ogni iterazione
è legata a una
diversa capitale
for ( (paese, capitale) <- capitali) print(capitale)
• Più precisamente
– una classe sealed ammette come uniche sottoclassi
quelle definite nello stesso file della classe-base
• Questo meccanismo è utile nel pattern matching
proprio per essere certi di aver coperto tutti i casi,
evitando di dover gestire un default "generico" o di dover usare
funzioni parziali.
IL TIPO Option
ESEMPIO 50: IL TIPO Option
Il tipo speciale Option esprime parametri opzionali
• due valori possibili: None e Some(qualcosa)
• particolarmente utile nel pattern matching ☺
PERCHÉ ?
• alternativa migliore, più "tipata", dell'approccio tipico
Java di passare null per un argomento mancante
– l'approccio Java è error prone: causa bug difficili da scovare,
riempie il codice di test if (arg==null), etc
– comunque non funzionerebbe in Scala, dove null è sottotipo
di AnyRef ma non di AnyVal
• così, errori semantici (mancanza di test arg==null)
diventano errori di tipo rilevabili dal compilatore
Letterale-funzione TOTALE
val f:
case
case
case
}
val f:
case
case
case
case
}
Int
x if
x if
x if
=> Int =
x>0 =>
x==0 =>
x<0 =>
{
x+4
7
-x
Option[Int] => Int
Some(x) if x>0 =>
Some(x) if x==0 =>
Some(x) if x<0 =>
None => 33
= {
x+4
7
-x
Valori "opzionali" sono prodotti da varie operazioni standard sulle
collection, come ad esempio l'estrazione di elementi da Mappe
(Some(x) se x esiste per quella chiave, None altrimenti)
ESEMPIO 51: IL TIPO Option
Valori "opzionali" sono prodotti da varie operazioni
standard sulle collection, come sulle Map
– Some(x) se x esiste per quella chiave
– None altrimenti
PROPRIETÀ
val capitali = Map(
"Francia" -> "Parigi",
"Germania" -> "Berlino",
"Italia" -> "Roma")
Valore che esiste
Valore che non esiste
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
7
PROPRIETÀ & METODI ACCESSOR
• In Java, le proprietà "non esistono" esplicitamente
– sono espresse da due metodi accessor getProp/setProp
che agiscono su una variabile di stato privata Prop
• In C#, sintassi speciale, ad hoc
• ln Scala, le proprietà esistono, ma senza richiedere
una sintassi speciale
ESEMPIO 52: PROPRIETÀ
La classe
class Orario {
var ora = 8
var minuti = 30
}
– semplicemente i metodi accessor sono generati in automatico
• Più precisamente:
– ogni volta che si definisce un val di nome prop viene
generato automaticamente anche l'accessor in lettura prop
Però, la versione estesa può
essere personalizzata
– ogni volta che si definisce un var di nome prop vengono
generati automaticamente sia l'accessor in lettura prop, sia
l'accessor in scrittura prop_=
• comportamenti "custom" per
i metodi
• espressioni require per
verifica precondizioni, etc
– il campo-dati prop è qualificato privato dell'oggetto
ESEMPIO 53: PROPRIETÀ & FIELD
Si possono definire accessor anche senza un campodati associato, tipicamente per fornire una "vista" diversa:
class Orario {
var ora = 8
var minuti = 30
}
è del tutto equivalente a
class Orario {
private[this] var h = 8
private[this] var m = 30
def ora = h
def ora_=(hh:Int) {h=hh}
def minuti = m
def minuti_=(mm:Int) {m=mm}
}
ESEMPIO 53: PROPRIETÀ & FIELD
Qui con anche un flag AM/PM e metodo oraUK in scrittura
class Orario {
var ora = 8
var minuti = 30
def oraUK = if (ora>0 && ora <13) ora else
if (ora==0) 12 else ora % 12
def minutiUK = minuti
private[this] var flag = "AM"
def am = flag=="AM"
def pm = flag=="PM"
def am_= (morning:Boolean) {
if (morning) flag="AM" else flag="PM" }
def oraUK_= (h:Int) {
if (flag=="AM") if (h!=12) ora=h else ora=0
else ora = h+12 }
}
class
var
var
def
Orario {
ora = 8
minuti = 30
oraUK = if (ora>0 && ora <13) ora
else if (ora==0) 12
else ora % 12
def minutiUK = minuti
}
• Aggiunta accessor "finto"
in lettura che restituisce
l'ora "English style"
PATTERN SENZA CASE CLASSES
• I pattern sono potenti, ma richiedono case classes
– altrimenti, la notazione Costruttore(args) non si può usare
• Però, farebbe comodo poter usare tale approccio
anche su oggetti di classe arbitraria
– ad esempio, String
EXTRACTOR
– o più in generale, istanze di classi che non sono case classes
perché non erano state pensate per tale scopo
• A tale scopo, Scala offre gli estrattori
– oggetti (non classi!) che definiscono un metodo unapply che
fa match con un valore e lo estrae
– concettualmente duale del metodo apply che invece lega
valori assieme (operatore ())
– spesso unapply e apply sono definiti assieme, ma non è un
obbligo: può benissimo esserci uno senza l'altro.
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
8
ESEMPIO 54: EXTRACTOR
• Supponiamo di aver definito la classe Persona
class Persona(val nome:String, val year:Int){
override def toString = nome + " è nato nel " + year
}
ESEMPIO 54: EXTRACTOR
• Per risolvere il problema, serve un extractor
– da definire fuori dalla classe Persona, in un oggetto
– ad esempio, nell'oggetto companion:
object Persona {
def unapply(p:Persona) : Option[(String,Int)] =
Some(p.nome,p.year) }
e di voler effettuare pattern matching su di essa, così:
def filtra(x : AnyRef) =
x match {
case Persona(n, a) => a
case _ => "non mi piace"
}
• Con questo, il pattern matching è accettato:
• SE fosse una case class, sarebbe già fatto.
• MA non lo è quella notazione non è ammessa:
def filtra(x : AnyRef) =
x match {
case Persona(n, a) => a
case _ => "non mi piace"
}
perché la scrittura x match {case Persona(n,a) si rimappa
su Persona.unapply(x) che "smembra" x come necessario.
EXTRACTOR IN PRATICA
• In pratica, il metodo extractor unapply
– deve stare in un oggetto (non in una classe)
– ritorna tipicamente un tipo opzionale, quindi
• Option[T] nel caso di un valore di ritorno solo Some(x)
• Option[(T1,T2,…)] nel caso di valori di ritorno multipli
Some((x1,x2,…)) abbreviabile come Some(x1,x2,…)
• None nel caso l'argomento non faccia match
– se non lega variabili può avere tipo di ritorno Boolean
• ovvero, nel caso nostro:
nome dell'oggetto extractor, da usare nel case
object Persona {
tipo di ritorno
def unapply(p:Persona) : Option[(String,Int)] =
Some(p.nome,p.year) }
nome della classe
valore ritornato
ESEMPIO 54: versione completa
class Persona(val nome:String, val year:Int){
override def toString = nome + " è nato nel " + year
}
object Persona {
def unapply(p:Persona) : Option[(String,Int)] =
Some(p.nome,p.year)
}
object Test {
def filtra(x : AnyRef) =
x match {
case Persona(n, a) => a
case _ => "non mi piace"
}
def main( args: Array[String]) {
val p = new Persona("John", 1984)
println(filtra(p))
}
}
class Persona(val nome:String, val year:Int){
override def toString = nome + " è nato nel " + year
}
object Umano {
def unapply(p:Persona) : Option[(String,Int)] =
Some(p.nome,p.year)
}
object Test {
def filtra(x : AnyRef) =
x match {
case Umano(n, a) => a
case _ => "non mi piace"
}
...
Indipendenza dalla rappresentazione
•
le case class la espongono
•
l'extractor no ☺
Però. l'extractor è meno efficiente John è nato nel 1984
John è nato nel 1984
1984
ESEMPIO 55: extractor con apply
ESEMPIO 54: variante
• Se si cambia nome all'oggetto che definisce unapply,
cambia il nome da usare nel pattern
// OK !
• Aggiungendo il metodo duale apply, si "finge" la
presenza di un costruttore, completando l'opera
object Umano {
def unapply(p:Persona) : Option[(String,Int)] =
Some(p.nome,p.year)
def apply(nome:String, year:Int) = new Persona(nome,year)
}
Metodo FACTORY che simula un costruttore
object Test {
...
def main( args: Array[String]) {
val p = Umano("John", 1984) // era: new Persona(..)
println(filtra(p))
DISACCOPPIAMENTO COMPLETATO
}
• il cliente vede solo Umano
}
1984
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
•
internamente si usa Persona
È così possibile cambiare la rappresentazione
interna in modo indipendente.
9
ESEMPIO 56: extractor ad arità variabile
• A volte è necessario che un extractor restituisca un
numero variabile di argomenti
• In tali casi, unapply è sostituito da unapplySeq
ESEMPIO 57: variante
• È anche possibile restituire un mix di argomenti
– alcuni fissi, altri variabili (in fondo)
• In tali casi, unapplySeq restituisce una tupla
– ossia restituisce Option[(T1,T2,…,Seq[T]]
– restituisce Option[Seq[T]] anziché Option[T]
object Splitter {
def unapplySeq(s:String) : Option[Seq[String]] = Some(s.split(" "))
}
object TestExtractorVarArg {
def main( args: Array[String]) {
val s = "La nebbia agli irti colli piovviginando sale"
println(s match {
case Splitter("La", _*) => "che poesia!"
case _ => "bleah"
Sequenza che inizia con "La"
})
}
che
}
poesia!
object SplitterMixed {
def unapplySeq(s:String) : Option[(Int,Seq[String])] =
Some(s.split(" ").length, s.split(" "))
}
object TestExtractorVarArg {
def main( args: Array[String]) {
val s = "La nebbia agli irti colli piovviginando sale"
println(s match {
case SplitterMixed(7, _*) => "che poesia!"
case SplitterMixed(n, _*) => if (n<6) "poesia corta"
case _ => "bleah"
})
1°elemento fisso lunghezza
}
che
poesia!
}
ESEMPIO 57: variante
• È anche possibile restituire un mix di argomenti
– alcuni fissi, altri variabili (in fondo)
• In tali casi, unapplySeq restituisce una tupla
– ossia restituisce Option[(T1,T2,…,Seq[T]]
object SplitterMixed {
def unapplySeq(s:String) : Option[(Int,Seq[String])] =
Some(s.split(" ").length, s.split(" "))
}
object TestExtractorVarArg {
5 elementi (anziché 7)
def main( args: Array[String]) {
val s = "La nebbia agli_irti_colli piovviginando sale"
println(s match {
case SplitterMixed(7, _*) => "che poesia!"
case SplitterMixed(n, _*) => if (n<6) "poesia corta"
case _ => "bleah"
L'output cambia!
})
1°elemento fisso lunghezza
}
poesia corta
EXTRACTOR & COLLECTIONS
• L'extractor è usato "a tappeto" nelle Scala collection
• In effetti, le notazioni che permettono di accedere ad
array e liste, come Array(…), List(…), sono tutte
realizzate con extractor (e injector)
• Ad esempio, pattern come List(…) sono possibili
perché il companion object di List definisce
object List {
def apply[T](elements: T*) = elements.toList
def unapplySeq[T](list: List[T]): Option[Seq[T]] = Some(list)
}
− apply permette di scrivere List(1,2), List(), con un
numero variabile di argomenti
− unapplySeq supporta l'uso di List(…) come pattern nei case
}
EXTRACTOR & REGEX
• L'extractor è usato anche nelle espressioni regolari
– Scala le eredita da Java, quindi stessa sintassi base
• In Scala però gli extractor permettono di fare match su
pezzi di stringhe descritte da espressioni regolari
– Ad esempio, la classe Decimal include un extractor che
separa segno, parte intera e parte decimale di un numero
– quindi, si può scrivere:
GRAFICA:
Swing "in salsa Scala"
val Decimal(s, i, d) = "-56.78"
val Decimal(s, i, d) = "123.45"
– ottenendo rispettivamente
s = "-"
i = "56"
d = "78"
s = null
i = "123"
d = "45"
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
10
Swing IN SCALA
• La grafica Scala si appoggia su Swing, come in Java
– stesse astrazioni, stesso approccio, ~ stessi componenti
– ma nomi ripuliti eliminata la "J" iniziale
– il plugin Scala per Eclipse supporta WYSIWYG
ESEMPIO 60: SWING MINI-APP
• Applicazione ultra-basic
import scala.swing._
object Swing1 extends SimpleSwingApplication {
Metodo chiamato sa SimpleSwingApplication per creare il top frame
• Però, scala.swing definisce un livello extra
def top = new MainFrame {
title = "Primo esempio"
contents = new Label("Etichetta bellissima")
}
– classe SimpleSwingApplication (SimpleGUIApplication
in Scala 2.8) che definisce l'essenziale di una applicazione
grafica – un main frame con alcune proprietà
– title è il titolo della finestra (proprietà con getter/setter)
}
– contents è il contenuto, di tipo Seq[Component]
• per i frame comunque il contenuto è unico, solitamente un pannello
• anche questa è una proprietà con getter/setter
• per aggiungere componenti, operatore +=
Swing IN SCALA: STRUTTURA
ESEMPIO 61: SWING MINI-APP
• Applicazione basic con pannello e bottone
• In Java
– tipicamente si crea un pannello e gli si attribuisce un layout
– si aggiungono componenti al pannello
• In Scala
import scala.swing._
object Swing2 extends SimpleSwingApplication {
def top = new MainFrame {
title = "Secondo esempio"
FlowPanel adotta FlowLayout
contents = new FlowPanel {
Button, NON JButton
contents += new Button {
text = "Cliccami, ti prego"
}
}
– la classe Panel è astratta
– le sottoclassi concrete adottano un layout: BorderPanel,
BoxPanel, FlowPanel, GridBagPanel, GridPanel
– si aggiungono componenti alla proprietà contents
}
Swing IN SCALA: EVENTI
• In Java
– ogni componente che possa generare eventi deve agganciarsi
a un listener specifico per il tipo di evento
– metodi della forma addXXXListener(handler)
invocati sull'oggetto che genera l'evento
– il listener deve implementare l'interfaccia XXXListener
ESEMPIO 62: SWING MINI-APP REATTIVA
• Applicazione basic con bottone reattivo
import scala.swing._
import scala.swing.event._
object Swing3 extends SimpleSwingApplication {
def top = new MainFrame {
title = "Secondo esempio"
contents = new FlowPanel {
val button = new Button {
text = "Cliccami, ti prego"
}
contents += button
Pannello ascolta il bottone
listenTo(button)
reactions += {
case ButtonClicked(b) => button.text = "Mi hai premuto :)"
}
}
– se è esterno al pannello, ciò crea molto boilerplate code
perciò spesso il listener è il pannello handler = this
• In Scala
– si rovescia il rapporto: è il listener che decide di ascoltare una
certa sorgente di eventi metodo listenTo (e deafTo)
– la gestione dell'evento si incapsula nella proprietà reactions
in cui si fa uso del pattern matching per discriminare
– minimo boilerplate code di gestione!
}
}
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
Gestione dell'evento
via pattern matching
11
ESEMPIO 63: SWING MINI-APP REATTIVA
ESEMPIO 64: SWING MINI-APP REATTIVA
• Applicazione basic con campo di testo
• Applicazione con due bottoni ("Rosso & Blu")
import scala.swing._
import scala.swing.event._
import scala.swing._
import scala.swing.event._
object Swing5 extends SimpleSwingApplication {
object Swing4 extends SimpleSwingApplication {
def top = new MainFrame{
title = "Rosso e blu"
contents = new FlowPanel {
val buttonRed = new Button { text = "Rosso" }
val buttonBlue = new Button { text = "Blu" }
contents += buttonRed; contents += buttonBlue
listenTo(buttonRed);
listenTo(buttonBlue)
reactions += {
case ButtonClicked(`buttonRed`) => this.background = Color.red
case ButtonClicked(`buttonBlue`) => this.background = Color.blue
}
}
Tipo dell'evento
SORGENTE DELL'EVENTO
def top = new MainFrame {
title = "Secondo esempio"
contents = new FlowPanel {
val textfield = new TextField {
text = "Inserire il nome"
columns = 20
}
contents += textfield
listenTo(textfield)
Tipo e sorgente
reactions += {
dell'evento
case EditDone(_) =>
if (textfield.text=="Paperone") textfield.text = "ZIETTO CARISSIMO!"
else textfield.text = "E chi ti conosce?"
}
BACKTICK NECESSARIO per evitare che la consideri
una variabile e faccia match con qualunque cosa
}
}
}}}
PARSER COMBINATORS
• Scala permette di specificare in modo diretto sintassi
& semantica di un proprio linguaggio, tramite appositi
combinatori
PARSER COMBINATORS per
Domain Specific Languages
– ~
esprime concatenazione
– rep(…)
– |
esprime ripetizione del contenuto
esprime alternativa
– parentesi
per raggruppare
• Tecnicamente:
– occorre importare scala.util.parsing.combinator._
•
•
fino a Scala 2.10, libreria già inclusa nella distribuzione standard
da Scala 2.11, è un jar accessorio da scaricare da Maven central
http://central.maven.org/maven2/org/scala-lang/modules/scalaparser-combinators_2.11/1.0.2/
– la propria classe deve estendere JavaTokenParsers
– per scatenare il parser, si invoca la funzione parseAll
ESEMPIO 65: MINI PARSER ARITMETICO
ESEMPIO 65: MINI PARSER ARITMETICO
import scala.util.parsing.combinator._
def main(args: Array[String]) {
val expr1 = "2 * (3 + 7)"
val expr2 = "2 * (3 + 7))"
println(parseAll(expr, expr1))
println(parseAll(expr, expr2))
println(parseAll(expr, "3+4"))
}
}
class
def
def
def
}
Arith1
expr:
term:
factor:
extends JavaTokenParsers {
Parser[Any] = term~rep("+"~term | "-"~term)
Parser[Any] = factor~rep("*"~factor | "/"~factor)
Parser[Any] = floatingPointNumber | "("~expr~")"
tipo di ritorno
object ParseExpr extends Arith1 {
def main(args: Array[String]) {
val expr1 = "2 * (3 + 7)"
val expr2 = "2 * (3 + 7))"
println(parseAll(expr, expr1))
println(parseAll(expr, expr2))
println(parseAll(expr, "3+4"))
}
}
//
//
//
//
//
GIUSTA
SBAGLIATA
GIUSTA
SBAGLIATA
GIUSTA
//
//
//
//
//
GIUSTA
SBAGLIATA
GIUSTA
SBAGLIATA
GIUSTA
[1.12] parsed:
((2~List((*~(((~((3~List())~List((+~(7~List())))))~)))))~List())
[1.12] failure: string matching regex `\z' expected
but `)' found 2 * (3 + 7))
[1.4] parsed: ((3~List())~List((+~(4~List()))))
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
12
PARSER DETERMINISTICI LL(1)
• Di default, il parser generato è in grado di supportare
il non determinismo, ossia gestisce anche grammatiche non LL(1) – a prezzo ovviamente di inefficienza
• Se però la grammatica è LL(1), si può specificarlo
usando l'apposito combinatore ~!
ESEMPIO 66: MINI PARSER ARITMETICO
import scala.util.parsing.combinator._
class
def
def
def
}
Arith1 extends JavaTokenParsers {
expr: Parser[Any] = term~!rep("+"~!term | "-"~!term)
term: Parser[Any] = factor~!rep("*"~!factor | "/"~!factor)
factor: Parser[Any] = floatingPointNumber | "("~!expr~!")"
– notare il ! che ricorda il "cut" del Prolog
• In tal caso, il parser generato sarà deterministico
– efficienza nettamente migliore
– impossibilità di fare backtracking
guai se la grammatica non è LL(1)
object ParseExpr extends Arith1 {
def main(args: Array[String]) {
val expr1 = "2 * (3 + 7)"
val expr2 = "2 * (3 + 7))"
println(parseAll(expr, expr1))
println(parseAll(expr, expr2))
println(parseAll(expr, "3+4"))
}
}
ESEMPIO 66: MINI PARSER ARITMETICO
import scala.util.parsing.combinator._
class
def
def
def
}
Arith1 extends JavaTokenParsers {
expr: Parser[Any] = term~!rep("+"~!term | "-"~!term)
term: Parser[Any] = factor~!rep("*"~!factor | "/"~!factor)
factor: Parser[Any] = floatingPointNumber | "("~!expr~!")"
[1.12] parsed:
((2~List((*~(((~((3~List())~List((+~(7~List())))))~)))))~List())
[1.12] failure: string matching regex `\z' expected
but `)' found 2 * (3 + 7))
//
//
//
//
//
GIUSTA
SBAGLIATA
GIUSTA
SBAGLIATA
GIUSTA
AZIONI SEMANTICHE
• Per inserire azioni semantiche occorre specificare una
funzione Scala accanto alla regola sintattica, usando
l'apposito combinatore ^^
• Occorre altresì stabilire il corretto tipo di ritorno delle
funzioni di parsing
– fin qui, si è usato sempre Parser[Any], che formalmente va
bene ma non è molto utile… (è come restituire Object..)
– perciò, ora le varie funzioni di parsing dovranno più opportunamente restituire, secondo i casi:
Parser[Float] e Parser[List[Float]] per i dati
[1.4] parsed: ((3~List())~List((+~(4~List()))))
Parser[(Float, Float) => Float] per gli operatori
Interessante: nella stampa, il combinatore è sempre ~ (non ~!)
ESEMPIO 67: MINI PARSER RPN
versione 0: puro riconoscitore di termini
// tratto da
// from http://bitwalker.org/blog/2013/08/10/learn-by-example-scala-parser-combinators/
import scala.util.parsing.combinator._
class Polish0 extends JavaTokenParsers {
def expr: Parser[Any] = rep(term ~ operator)
Azione semantica
def term: Parser[List[Float]] = rep(num)
def num: Parser[Float] = floatingPointNumber ^^ (_.toFloat)
ESEMPIO 67: MINI PARSER RPN (v0)
object Parser0 extends Polish0 {
def main(args: Array[String]) {
Una sequenza di term
val result1 = parseAll(term, "5 1")
println(s"Parsed $result1")
Un'espressione
val result2 = parseAll(expr, "5 1 +")
(sequenza di term seguita
da un operator)
println(s"Parsed $result2")
}
}
Parsed [1.4] parsed: List(5.0, 1.0)
def operator: Parser[(Float, Float) => Float] =
("*" | "/" | "+" | "-") ^^ {
Azione semantica
case "+" => (x, y) => x + y
case "-" => (x, y) => x - y
case "*" => (x, y) => x * y
case "/" => (x, y) => if (y > 0) (x / y) else 0.0f
}
}
Risultato: sequenza di Float
Parsed [1.6] parsed: List((List(5.0, 1.0)~<function2>))
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
Risultato: sequenza di Espressioni
ciascuna delle quali è
una concatenazione fra
-
una lista di termini
-
una funzione-operatore (binaria)
13
ESEMPIO 68: MINI PARSER RPN
versione 1: con semantica espressioni
// tratto da
// from http://bitwalker.org/blog/2013/08/10/learn-by-example-scala-parser-combinators/
import scala.util.parsing.combinator._
Cambia tipo di ritorno
class Polish1 extends JavaTokenParsers {
def expr: Parser[Float] = rep(term ~ operator) ^^ {
case terms =>
SEMANTICA:
valutazione
su stack
var stack = List.empty[Float]
var lastOp: (Float, Float) => Float = (x, y) => x + y
terms.foreach(t => t match {
Funzione ausiliaria
case nums ~ op => lastOp = op;
stack = reduce(stack ++ nums, op)
})
stack.reduceRight( (x,y) => lastOp(y,x) )
}
...
}
ESEMPIO 68: MINI PARSER RPN
versione 1: con semantica espressioni
La funzione ausiliaria opera come una Arithmetic Stack Machine
• per usare più facilmente il pattern matching, ribalta la lista
(quindi l'operatore va in testa)
• distingue tre casi: che ci siano due numeri (e allora inserisce
l'operazione), che ce ne sia uno solo (lo lascia com'è) o che la lista
sia vuota (e allora inserisce la lista vuota)
def reduce(
nums: List[Float],
op: (Float, Float) => Float): List[Float] = {
val result = nums.reverse match {
case x :: y :: xs => xs ++ List(op(y, x))
case List(x)
=> List(x)
case _
=> List.empty[Float]
}
result
}
ESEMPIO 69:
un piccolo scanner per identificatori
ESEMPIO 68: MINI PARSER RPN (v1)
object Calc1 extends Polish1 {
def main(args: Array[String]) {
val result = parseAll(expr, "5 1 +")
println(s"Parsed $result")
}
}
Parsed [1.6] parsed: 6.0
Una espressione corretta
import scala.util.parsing.combinator._
object ParseRegEx extends JavaTokenParsers {
def main(args: Array[String]) {
.r genera il parser!
val ident: Parser[String] = """[a-zA-Z_]\w*""".r
println(parseAll(ident, "a34")) // OK
println(parseAll(ident, "34a")) // NO
Risultato: un singolo Float
Regular expression
}
}
Ovviamente, se si fosse tentato riconoscere un numero, avrebbe detto NO:
def main(args: Array[String]) {
NON è un numero!
val result = parseAll(num, "5 1 +")
println(s"Parsed $result")
}
}
[1.4] parsed: a34
[1.1] failure: string matching regex `[a-zA-Z_]\w*' expected
but `3' found
Parsed [1.3] failure: string matching regex `\z' expected but `1' found
5 1 +
Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015
14