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