interoperabilità scala / java

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