Dopo aver presentato brevemente i progetti Coin e Jigsaw nel numero scorso, iniziamo l’analisi delle tanto attese espressioni Lambda il cui obiettivo ultimo consiste nell’introdurre la programmazione funzionale in Java: progetto ambizioso, complesso e non privo di limiti e ripensamenti. Allo stato attuale Java SE 8 è ancora in fase di sviluppo e pertanto diverse componenti potrebbero subire delle variazioni. Questi argomenti sono presenti anche nell’ebook ‘Verso Java 8. Appunti per lo sviluppatore in Java SE7’ che a fine mese renderemo disponibile su questo sito.
Introduzione
Con questo articolo proseguiamo la nostra esplorazione di Java SE 8 iniziata con l’articolo precedente dove abbiamo presentato la parte del progetto Coin (“moneta”) esclusa da Java SE 7 e che pertanto dovrebbe essere, in forme ancora da definire, incluso in Java SE8, e abbiamo visto Jigsaw (“puzzle”), ossia la modularizzazione di Java, il quale sembrerebbe destinato ad un ulteriore discope, con qualche imbarazzo in casa Oracle.
Ma le novità (che arrivino con SE 8 oppure no), non finiscono qui. Il progetto, Lambda, come è lecito attendersi, ha creato e sta creando grande interesse nella comunità Java: la stragrande maggioranza delle persone considera la programmazione funzionale il futuro naturale di Java. Questo interesse è evidenziato da numerosi dibattiti nei quali non è infrequente imbattersi in commenti poco entusiastici dello stato del progetto Lambda. Commenti spesso scritti da persone che desideravano un supporto decisamente più spinto, più vicino a soluzioni compatibili con quelle messe in atto da Scala e pertanto deluse da quello che viene considerato un tentativo piuttosto timido di introduzione della programmazione funzionale.
Per formulare un giudizio ben fondato è tuttavia necessario sempre considerare che Java è ormai un linguaggio maturo e che questa maturità porta con se’ un notevole bagaglio di esperienze (tra l’altro non sempre centrate al primo tentativo… anzi) e di versioni da dover supportare. Tutto ciò genera una serie di pastoie non indifferenti con cui ogni nuova feature deve fare i conti. L’evoluzione controllata di linguaggi come Java non è un esercizio da prendere alla leggera ma richiede un lavoro “chirurgico” necessario per muoversi negli angusti spazi delimitati dai tanti vincoli tra i quali impera uno dei cavalli di battaglia di Java: la back compatibility a tutti i costi. La retrocompatibilità è di sicuro un ottimo principio, ma, se è portato all’esasperazione, molto probabilmente crea più problemi che vantaggi: per esempio si sente ancora la necessità di Vector?
Progetto Lambda in breve
La storia del progetto Lambda (chiamato anche informalmente “closures” e “anonymous methods“) non è stata delle più semplici: proposto inizialmente per essere rilasciato con Java SE 7, dopo diversi importanti ritardi, dovuti ad accese discussioni all’interno della comunità, è stato alla fine posticipato a Java SE 8, secondo i dettami del famoso piano B. Il progetto Lambda è guidato da Brian Goetz, Java Language Architect alla Oracle Corporation, in precedenza Senior Engineer alla Sun Microsystem, autore di innumerevoli pubblicazioni tecniche di grande interesse e coautore di uno dei migliori libri sul multi-threading in Java [2].
L’obiettivo ultimo del progetto Lambda consiste nell’introdurre in Java modelli di programmazione che permettono di modellare il codice come strutture dati in modo idiomatico (nativo, peculiare) e semplificato. Si tratta di una “filosofia”, nota con i termini Inglesi “Code as data”, che prevede di strutturare il codice sorgente come una struttura di dati base, e quindi come un tipo primitivo, che il linguaggio di programmazione conosce e sa manipolare. Questo principio, come illustrato di seguito, ha delle ripercussioni molto importanti, come per esempio la possibilità di assegnare una porzione di codice a una variabile o fornirla ad una funzione per poterla manipolare.
Interfacce funzionali: le espressioni Lambda non sono oggetti
La prima scelta di disegno alla base dell’intero paradigma funzionale Java verteva sullo stabilire cosa fossero, in termini grammaticali, le espressioni Lambda. Molti membri della comunità Java propendevano per considerarle come semplici istanze di classi annidate. Il grande vantaggio di questo approccio è che sarebbe estremamente semplice, giacche’ il tutto si risolveva nel modificare poco o nulla e nell’utilizzare la semantica esistente.
Dopo le inevitabili lunghe discussioni si è deciso di spingere verso una definizione diversa e meno conservativa. La posizione di Oracle, tra l’altro, è molto chiara: Java deve evolvere, con molta attenzione ovviamente, perche’ come per una una qualsiasi “specie vivente” l’evoluzione è imperativo categorico per la sopravvivenza. Pertanto il team di lavoro si è orientato verso la direzione che le espressioni Lambda non sono oggetti, posizione, quella degli oggetti, che finirebbe per chiudere la porta a un certo numero di potenziali feature utili per l’evoluzione del linguaggio.
La decisione di percorrere la via delle funzioni, approccio decisamente più lungimirante, ha però un impatto significativo sulla piattaforma Java.
Le caratteristiche
Vediamo anzitutto le nuove principali features:
- Interfacce funzionali: interfacce che contengono un solo metodo astratto.
- Espressioni Lambda come metodi. Le espressioni Lambda presentano molte analogie con il concetto di metodo: entrambi definiscono un elenco formale di parametri e un corpo (body). Tuttavia, avere espressioni Lambda come metodi richiede cambiamenti importanti al linguaggio come illustrato nei punti seguenti.
- Riferimenti ai metodi. Questa feature permette di fornire il riferimento di un metodo a un altro metodo senza necessariamente doverlo invocare immediatamente ma solo quando necessario.
- Nuove regole per la determinazione dei tipi di destinazione. In molti casi, il compilatore è in grado di dedurre il tipo di destinazione attraverso il meccanismo del type inference (caso analogo alla feature diamond dei Generics), il che significa che la stessa espressione può restituire tipi diversi in diversi contesti. Il tipo di ritorno viene convertito automaticamente nel tipo di destinazione corretto. Questa feature è di importanza fondamentale per poter implementare espressioni lambda generiche da fornire a metodi che devono operare su tipi di dati specifici.
JDK8
Al momento in cui viene redatto questo articolo, JDK8 è disponibile esclusivamente come preview [4] e non è ancora supportato da IDE come Eclipse. Quindi, è necessario tornare alle origini e compilare i sorgenti (.java) con il comando javac ed eseguire le classi (.class) con il comando java.
Classi annidate anonime
Nella programmazione OO il concetto di oggetto tende a essere relativamente pesante: si tratta di istanze di classi dichiarate in modo separato che incapsulano una serie di campi e di metodi che agiscono su di essi. Eppure, esistono delle classi Java la cui unica responsabilità consiste nel definire un singolo metodo (questo è il classico caso delle interfacce callback), che per molti versi può essere visto quasi come una funzione. Si consideri l’implementazione dell’interfaccia ActionListener riportata di seguito:
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
Una strategia frequentemente utilizzata dai programmatori Java per evitare di dover implementare un’apposita classe solo per dar luogo ad un’unica istanza utilizzata in un solo caso da una sola classe consiste nel ricorrere al costrutto delle classi annidate anonime (anonymous inner class), come mostrato di seguito:
myButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
});
Questo pattern è adottato da diverse librerie anche nel contesto della programmazione multi-threading dove è necessario assicurarsi che la parte da eseguire in parallelo sia indipendentemente dal thread che la eseguirà. Come visto nell’articolo degli aggiornamenti relativi al framework della concorrenza (cfr. [3]) , il dominio del calcolo parallelo è di particolare interesse, perche’ attualmente i produttori di CPU sembrerebbero puntare sul miglioramento delle prestazioni ottenuto attraverso la proliferazione di processori core piuttosto che cercare di ottenere incrementi del 0.5% dal singolo processore.
Vista la crescente importanza del modello “callback” e altri idiomi basati sullo stile funzionale, è importante che Java sia in grado di supportare questi pattern nel modo più leggero possibile. Allo stato attuale, ciò è possibile attraverso le classi annidate anonime che però soffrono di una serie di limitazioni di cui alcune sono riportate di seguito:
- sebbene la sintassi sia più leggera rispetto alla definizione di opportune classi, resta comunque prolissa soprattutto se paragonata alle funzioni;
- esiste una certa confusione in merito al significato dei nomi e dell’utilizzo della parola chiave “this“;
- la semantica relativa al class loading e alla creazioni di istanze presenta una certa rigidità;
- il compilatore non riesce ad identificare variabili locali non dichiarate final;
- impossibilità di astrarre il controllo del flusso.
Il progetto Lambda si occupa di affrontare diversi di questi problemi. In particolare, si occupa di:
- rimuove i punti 1 e 2 attraverso l’introduzione di nuove forme molto concise di espressione con associate regole di ambito locale;
- eludere il punto 3 attraverso la definizione della semantica delle nuove espressioni in modo più flessibile;
- migliorare il punto 4 consentendo al compilatore di dedurre l’uso final delle variabili.
Uno sguardo alle interfacce funzionali
L’interfaccia ActionListener, mostrata poco sopra, ha un solo metodo: si tratta di un pattern comune a molte interfacce “callback”. Altri esempi sono java.lang.Runnable e java.util.Comparator (per la precisione Comparator include anche il metodo equals che però è considerato irrilevante in questo contesto giacche’ si tratta della ripetizione di un metodo definito nella classe Object).
Le interfacce funzionali sono esattamente tutte queste interfacce caratterizzate da un solo metodo. In precedenza erano chiamate tipi SAM: Single Abstract Method, metodo astratto singolo. Questa definizione non va interpretata rigidamente. Vengono considerate interfacce funzionali anche quelle che prevedono diverse versioni del metodo che dichiarano (overloading) o che ereditano da super-interfacce.
Identificazione
La dichiarazione di un’interfaccia funzionale non richiede speciali accorgimenti: il compilatore è in grado di identificarle come tale, in base ad un’attenta analisi della relativa struttura. Per la precisione, questo processo di identificazione è un po’ più complesso di un semplice conteggio dei metodi presenti nella dichiarazione. L’interfaccia potrebbe dar luogo alla dichiarazione di diverse versioni del metodo, ereditare da diversi genitori diversi metodi che però da un punto di vista logico rappresentano lo stesso, o anche dichiarare in maniera ridondante metodi forniti automaticamente dalla classe Object.
Un’alternativa ai tipi di funzione, proposta inizialmente, consiste nell’introdurre un nuovo tipo di struttura funzione. Per esempio, un tipo come una funzione che dalla coppia (String, Object) restituisca un int si sarebbe potuto esprimere come (String, Object) -> int. Questa idea è stata esaminata e respinta a causa di alcuni svantaggi, come per esempio incremento di complessità nella gestione dei tipi, divergenza nella struttura delle librerie, sintassi pesante, incremento della complessità a runtime, etc. È stato deciso di scegliere l’approccio di “usare ciò che si conosce“, dal momento che le librerie esistenti utilizzano abbondantemente interfacce funzionali, si è deciso di codificare e sfruttare questo modello.
Alcune interfacce funzionali
Alcuni esempi di interfacce funzionali esistenti sono:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
Esempi di espressioni Lambda
La critica più severa rivolta alle classi anonime annidate è la prolissità. Esse soffrono di un problema definito “verticale”: l’istanza ActionListener richiede cinque righe di codice sorgente per incapsulare una singola istruzione. Le espressioni Lambda sono metodi anonimi disegnati per affrontare il “problema verticale”, sostituendo il meccanismo di classi interne anonime con un approccio più leggero. Di seguito sono riportati alcuni esempi delle espressioni Lambda.
La prima si occupa di restituire la dimensione della stringa fornita in input:
s -> s.length()
La seconda esegue la somma dei due valori interi forniti in input:
(int x, int y) -> x + y
La terza e la quarta sono due forme equivalenti; non richiedono alcun argomento e producono in output il numero 42:
() -> 42
() -> { return 42; }
La quinta di occupa di stampare la stringa di input su console:
(String s) -> {
System.out.println(s);
}
La sesta non ha parametri, include la sola invocazione al gc (Garbage Collector), e restituisce un valore void:
() -> { System.gc(); }
Per finire,l’ultima restituisce sempre il valore della prima variabile, tuttavia è stata inclusa per mostrare che il corpo di un’espressione Lambda può essere codificato come qualsiasi metodo:
(x, y, z) -> {
if (true) {
return x;
} else {
int result = y;
for (int i = 1; i < z; i++) {
result *= i;
}
return result;
}
}
Sintassi
La sintassi generale prevede un elenco di argomenti, l’operatore freccia ->, e un corpo che può essere sia una singola espressione, sia un blocco di istruzioni. Da notare che la sintassi prescelta è la stessa utilizzata da C# e Scala. Le motivazioni ufficiali recitano che ciò è dovuto al fatto che non è stato possibile identificare un’alternativa migliore. Inoltre questa sintassi offre una serie di vantaggi evidenti: è già conosciuta da molti programmatori, semplifica l’interoperabilità con Scala, etc.
Nella forma semplice basata su una singola espressione, l’esecuzione è molto semplice: il corpo viene valutato e quindi viene restituito. Nella forma a blocchi la situazione non cambia di molto, il corpo viene valutato come un qualsiasi metodo: l’istruzione return fa sì che il controllo torni al chiamante. Parole chiave come break e continue non sono consentite al livello superiore, mentre sono naturalmente permesse all’interno di cicli: sono vietati salti non locali. Infine, se il corpo produce un risultato, ogni percorso di controllo deve restituire un valore di quel tipo o scatenare un’apposita eccezione.
La sintassi è stata ottimizzata per il caso comune in cui il corpo di un’espressione Lambda è piuttosto ridotto. Ad esempio, nella forma singola espressione, viene eliminata la necessità di dover includere la parola chiave return.
Le espressioni lambda, come è logico attendersi, sono utilizzate frequentemente in invocazioni annidate: l’argomento di una chiamata è il risultato di un’altra espressione Lambda. In questi casi è possibile minimizzare l’overhead dovuto a delimitatori superflui. Tuttavia, è ancora consentito il ricorso alle parentesi per tutte quelle situazioni in cui è utile delimitare l’intera espressione, così come per qualsiasi altra espressione. Di seguito sono mostrati alcuni esempi:
FileFilter java =
(File f) -> f.getName().endsWith(".java");
String user =
doPrivileged(() -> System.getProperty("user.name"));
new Thread(() -> {
connectToService();
sendNotification();
}).start();
Conclusione
Con questo articolo abbiamo continuato il percorso di esplorazione di Java SE 8 e in particolare ci siamo addentrati nel campo delle espressioni Lambda. Abbiamo presentato brevemente il progetto Lambda, le caratteristiche e le scelte iniziali. Per adesso vale la pena sottolineare alcune scelte coraggiose effettuate dal team di lavoro (attitudine utilizzata parsimoniosamente) a partire dalla decisione di non considerare le espressioni Lambda come particolari tipi di oggetti. Abbiamo poi passato in rassegna un concetto fondamentale per la programmazione funzionale in Java: le interfacce funzionali, interfacce, cioè che contengono un solo metodo astratto.
Nel corso del prossimo capitolo presenteremo in dettaglio le espressioni Lambda, mostrandone luci e inevitabili ombre e quindi saremo in grado di illustrare in maniera completa le conclusioni finali.
Riferimenti
[1] Luca Vetti Tagliati, “L’evoluzione di Java: verso Java 8 – III parte: Java SE 7 e la “moneta” delle nuove feature”, MokaByte 173, maggio 2012
https://www.mokabyte.it/cms/article.run?permalink=mb173_javaevolution-3
[2] Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea, “Java Concurrency in Practice”, Addison-Wesley, 2006
[3] Luca Vetti Tagliati, “L’evoluzione di Java: verso Java 8 – IV parte: IV parte: Java SE 7, fork et impera”, MokaByte 174, giugno 2012
https://www.mokabyte.it/cms/article.run?permalink=mb174_javaevolution-4
[4] La preview di JDK 8