Università degli Studi di Bologna Scuola di Ingegneria e Architettura Capitolo 24 Scala: argomenti avanzati 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 Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 1 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À 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" ☺ Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 2 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À: 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 3 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 • che però ha problemi con le specifiche di unità ("E:\...") • In alternativa, si può importare una classe Java con l'istruzione Scala import • 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. 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 • 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…) Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 4 CONVERSIONI AUTOMATICHE DI STRUTTURE DATI JAVA / SCALA • Nonostante i nomi analoghi, le classi-collection Scala non sono le stesse definite da Java • 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 • Altre conversioni invece funzionano in un solo verso: Nel senso opposto: UnsupportedOperationException • 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) Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 5 CONVERSIONI AUTOMATICHE DI STRUTTURE DATI JAVA / SCALA Esempio con JavaConversions importa le versioni mutevoli ArrayBuffer Scala in List Java… … e ritorno, da List Java a Seq Scala Idem con mappe: una mappa Scala (sopra) convertita in Map Java (sotto) INTEROPERABILITÀ SCALA / JAVA: UN PROBLEMA DI.. TIPI • Problema: Java prevede wildcard e tipi "raw", Scala no • 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 } dove dichiarazioni è un elenco di val e type • 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) Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 6 TIPI ESISTENZIALI: MOTIVAZIONE • A cosa servono? • a dare un "corrispondente Scala" ai tipi Java con wildcard 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: sintassi shortcut per tipo esistenziale TIPI ESISTENZIALI: NAMING ISSUE • Ma… il tipo esistenziale come si chiama? • NON SI CHIAMA, perché non ha nome • quindi è impossibile referenziarlo direttamente 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. Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 7 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 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 & PATTERN MATCHING Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 8 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 9 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) 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) Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 10 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: PATTERN MATCHING Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 11 ESEMPIO 40 (revised) – CON PACKAGE OSSERVA: uscendo dallo scope delle parentesi graffe, si torna automaticamente al namespace esterno (default) In questo modo è più semplice tenere più versioni del file nello stesso progetto (senza package, le classi risulterebbero duplicate) ESEMPIO 41: PATTERN MATCHING Estendiamo l'esempio per catturare altri pattern 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 Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 12 ESEMPIO 41: PATTERN MATCHING 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à ! case UnaryExp("+", e @ Number(v)) => e ESEMPIO 41 – CON PACKAGE Ora semplifica anche: –(Number(-3.6)) in Number(3.6) +(Number(-3.7)) in Number(-3.7) –(Number(3.9)) in Number(-3.9) Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 13 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 _ => } println(filtra( println(filtra( println(filtra( println(filtra( println(filtra( println(filtra( println(filtra( println(filtra( println(filtra( println(filtra( = "Lista "Lista "Tupla x + " di 3, inizia per 1" che inizia per 2" di due elementi" non mi piace" 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 Argomenti ripetuti catturano liste di qualsiasi lunghezza (maggiore di 1) [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 43: TYPED PATTERN I pattern possono anche essere tipati, sostituendo così cast, instanceof e altri test sul tipo di un oggetto. def filtraPerTipo(x x match { case s: String case y: Map[_,_] case _ } : Any) = => operazione sulla stringa s => operazione sulla mappa y => operazione di default Ad esempio, concretamente: def filtraPerTipo(x : x match { case s: String => case y: (u,v) => case i: Int => case _ => } Any) = s.length y._1 i x s e x sono riferimenti identici, ma di tipo diverso: s = (String) 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 Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 14 ESEMPIO 44: TYPED PATTERN .. 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 _ => … } 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 Non può verificarlo! 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) 3 ciao ESEMPIO 45: PATTERN MATCH SEMANTICO Se il match sintattico non basta guardie semantiche • utili per superare i limiti del pattern matching lineare • espressioni della forma if condizione davanti a => ESEMPIO: intercettare espressioni come x+x semplificandole in x*2 • 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)); SI, perché sono due variabili con guardia semantica Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 15 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 match { case x if x>0 => x+4 case x if x==0 => 7 case x if x<0 => -x } 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 } 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: val (u, w) = coppia • ottenendo in risposta Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 16 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 – 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: Int case x if case x if case x if } => Int = { x>0 => x+4 x==0 => 7 x<0 => -x Definizioni alternative tramite case multipli 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)) } val action : Any => Unit = { case s:Int => print("intero " + s) case List(n) => print("lista " + n) case a:String => print("msg "+ a) } Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 Lista di UN SOLO elemento 17 PATTERN MATCHING e FUNZIONI PARZIALI • 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 – da non confondere con le funzioni parzialmente applicate!! – 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.. • …ma al costo di fare due volte lo stesso controllo! ESEMPIO 49: FUNZIONI PARZIALI Letterale-funzione TOTALE val f: Int case x if case x if case x if } => Int = { x>0 => x+4 x==0 => 7 x<0 => -x Letterale-funzione PARZIALE val f: PartialFunction[Int,Int] = { case x if x>0 => x+4 case x if x==0 => 7 case x if x<0 => -x } Stesso output, perché in realtà questa funzione è definita in TUTTI i casi 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: Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 18 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 ☺ 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) CLASSI SEALED (SIGILLATE) • 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 • 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. Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 19 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 ESEMPIO 50: IL TIPO Option Letterale-funzione TOTALE val f: Int case x if case x if case x if } val f: case case case case } => Int = { x>0 => x+4 x==0 => 7 x<0 => -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) Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 20 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 val capitali = Map( "Francia" -> "Parigi", "Germania" -> "Berlino", "Italia" -> "Roma") Valore che esiste Valore che non esiste PROPRIETÀ Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 21 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 – 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 – 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_= – il campo-dati prop è qualificato privato dell'oggetto ESEMPIO 52: PROPRIETÀ La classe 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} } Però, la versione estesa può essere personalizzata • comportamenti "custom" per i metodi • espressioni require per verifica precondizioni, etc Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 22 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 } 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" 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 } } Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 23 EXTRACTOR 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 – 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 24 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 } 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" } • SE fosse una case class, sarebbe già fatto. • MA non lo è quella notazione non è ammessa: 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) } • Con questo, il pattern matching è accettato: def filtra(x : AnyRef) = x match { case Persona(n, a) => a // OK ! case _ => "non mi piace" } perché la scrittura x match {case Persona(n,a) si rimappa su Persona.unapply(x) che "smembra" x come necessario. Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 25 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)) } } John è nato nel 1984 1984 Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 26 ESEMPIO 54: variante • Se si cambia nome all'oggetto che definisce unapply, cambia il nome da usare nel pattern 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 1984 ESEMPIO 55: extractor con apply • 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 } • internamente si usa Persona È così possibile cambiare la rappresentazione interna in modo indipendente. Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 27 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 – 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! 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 { 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 } Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 poesia! 28 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 Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 29 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: 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" GRAFICA: Swing "in salsa Scala" Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 30 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 • Però, scala.swing definisce un livello extra – 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 += ESEMPIO 60: SWING MINI-APP • Applicazione ultra-basic import scala.swing._ object Swing1 extends SimpleSwingApplication { Metodo chiamato sa SimpleSwingApplication per creare il top frame def top = new MainFrame { title = "Primo esempio" contents = new Label("Etichetta bellissima") } } Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 31 Swing IN SCALA: STRUTTURA • In Java – tipicamente si crea un pannello e gli si attribuisce un layout – si aggiungono componenti al pannello • In Scala – la classe Panel è astratta – le sottoclassi concrete adottano un layout: BorderPanel, BoxPanel, FlowPanel, GridBagPanel, GridPanel – si aggiungono componenti alla proprietà contents ESEMPIO 61: SWING MINI-APP • Applicazione basic con pannello e bottone 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" } } } Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 32 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 – 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! 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 :)" } } } } Gestione dell'evento via pattern matching Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 33 ESEMPIO 63: SWING MINI-APP REATTIVA • Applicazione basic con campo di testo import scala.swing._ import scala.swing.event._ object Swing4 extends SimpleSwingApplication { 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?" } }}} ESEMPIO 64: SWING MINI-APP REATTIVA • Applicazione con due bottoni ("Rosso & Blu") import scala.swing._ import scala.swing.event._ object Swing5 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 } } BACKTICK NECESSARIO per evitare che la consideri una variabile e faccia match con qualunque cosa Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 34 PARSER COMBINATORS per Domain Specific Languages PARSER COMBINATORS • Scala permette di specificare in modo diretto sintassi & semantica di un proprio linguaggio, tramite appositi combinatori – ~ 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 Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 35 ESEMPIO 65: MINI PARSER ARITMETICO import scala.util.parsing.combinator._ class Arith1 extends JavaTokenParsers { def expr: Parser[Any] = term~rep("+"~term | "-"~term) def term: Parser[Any] = factor~rep("*"~factor | "/"~factor) def 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 ESEMPIO 65: MINI PARSER ARITMETICO 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 [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 36 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 ~! – 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) ESEMPIO 66: MINI PARSER ARITMETICO import scala.util.parsing.combinator._ class Arith1 extends JavaTokenParsers { def expr: Parser[Any] = term~!rep("+"~!term | "-"~!term) def term: Parser[Any] = factor~!rep("*"~!factor | "/"~!factor) def factor: Parser[Any] = floatingPointNumber | "("~!expr~!")" } 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 Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 37 ESEMPIO 66: MINI PARSER ARITMETICO import scala.util.parsing.combinator._ class Arith1 extends JavaTokenParsers { def expr: Parser[Any] = term~!rep("+"~!term | "-"~!term) def term: Parser[Any] = factor~!rep("*"~!factor | "/"~!factor) def 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)) [1.4] parsed: ((3~List())~List((+~(4~List())))) Interessante: nella stampa, il combinatore è sempre ~ (non ~!) 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 Parser[(Float, Float) => Float] per gli operatori Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 38 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) 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 } } 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) Risultato: sequenza di Float Parsed [1.6] parsed: List((List(5.0, 1.0)~<function2>)) Risultato: sequenza di Espressioni ciascuna delle quali è una concatenazione fra - una lista di termini - una funzione-operatore (binaria) Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 39 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 } Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 40 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 Risultato: un singolo Float 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") } } Parsed [1.3] failure: string matching regex `\z' expected but `1' found 5 1 + ESEMPIO 69: un piccolo scanner per identificatori 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 Regular expression } } [1.4] parsed: a34 [1.1] failure: string matching regex `[a-zA-Z_]\w*' expected but `3' found Prof. Enrico Denti - Università di Bologna – A.A. 2014/2015 41