Voi siete qui: Inizio Programmare in Scala

Da zero a sessanta: una introduzione a Scala

Perché Scala?

Le moderne applicazioni industriali e di rete devono conciliare un certo numero di interessi. Devono essere implementate velocemente e in maniera affidabile; devono supportare l’aggiunta di nuove funzioni in cicli di sviluppo brevi e incrementali; oltre a fornire la logica applicativa, devono offrire un accesso sicuro, un modello di persistenza dei dati, un comportamento transazionale e altre caratteristiche avanzate. Le applicazioni devono garantire un’alta disponibilità e un’elevata scalabilità, per le quali è necessaria una progettazione che supporti concorrenza e distribuzione. Le applicazioni sono collegate in rete e forniscono interfacce per essere usate sia da persone sia da altre applicazioni.

Per affrontare queste sfide, molti sviluppatori stanno cercando nuovi linguaggi e nuovi strumenti. I venerabili pilastri come Java, C# e C++ non sono più ottimali per sviluppare la prossima generazione di applicazioni.

Se siete programmatori Java…

Java è stato ufficialmente presentato da Sun Microsystems nel maggio del 1995, proprio mentre l’interesse per Internet si stava diffondendo. Java venne immediatamente accolto come un linguaggio ideale per scrivere applicazioni basate sul browser, una piattaforma che aveva bisogno di un linguaggio applicativo sicuro, portabile e facile da usare per gli sviluppatori. Il linguaggio regnante in quel momento, C++, non era adatto a questo dominio.

Oggi Java viene spesso usato per applicazioni lato server ed è uno dei linguaggi più popolari attualmente in uso per lo sviluppo di applicazioni web e di tipo industriale.

Tuttavia, Java è figlio del suo tempo e ora comincia a sentire il peso degli anni. Nel 1995 Java forniva una sintassi abbastanza simile al C++ da attirarne gli sviluppatori, evitando nel contempo molte delle mancanze e delle caratteristiche “spigolose” di quel linguaggio. Java adottò le idee più utili per i problemi di sviluppo della sua epoca, come la programmazione orientata agli oggetti (OOP), scartando alcune tecniche problematiche, come la gestione manuale della memoria. Queste scelte di progettazione trovarono un equilibrio eccellente che minimizzava la complessità e massimizzava la produttività degli sviluppatori, sacrificando in cambio le prestazioni nel confronto con il codice compilato nativamente. Nonostante Java si sia evoluto dalla sua nascita, molte persone ritengono che sia diventato troppo complesso senza affrontare adeguatamente alcune delle sfide di sviluppo più recenti.

Gli sviluppatori vogliono linguaggi che siano più sintetici e flessibili per migliorare la propria produttività. Questa è una delle ragioni per cui i cosiddetti linguaggi “di scripting” come Ruby e Python sono recentemente diventati più popolari.

L’infinito bisogno di scalabilità sta guidando le architetture verso la concorrenza pervasiva. Tuttavia, il modello di concorrenza di Java, che si basa sull’accesso sincronizzato a uno stato condiviso e mutabile, genera programmi complessi e soggetti a errori.

Sebbene il linguaggio Java senta il peso degli anni, la Java Virtual Machine (JVM) che lo esegue continua a brillare. Le ottimizzazioni effettuate dall’odierna JVM sono straordinarie e in molti casi permettono al bytecode di superare le prestazioni del codice compilato nativamente. Oggi molti sviluppatori credono che usare la JVM con nuovi linguaggi sia la strada da percorrere. Sun sta assecondando questa tendenza dando lavoro a molti degli sviluppatori principali di JRuby e Jython,1 che sono rispettivamente conversioni di Ruby e Python per la JVM.

L’attrattiva di Scala per lo sviluppatore Java è quella di un nuovo linguaggio più moderno capace di sfruttare le prestazioni incredibili della JVM e la ricchezza delle librerie Java che sono state sviluppate per oltre una decade.

Se siete programmatori Ruby, Python, &c…

Linguaggi dinamicamente tipati come Ruby, Python, Groovy, JavaScript e Smalltalk offrono una produttività molto alta grazie a una potente metaprogrammazione e a caratteristiche come eleganza e flessibilità.

Nonostante i loro vantaggi di produttività, i linguaggi dinamici potrebbero non essere la scelta migliore per tutte le applicazioni, in particolare per basi di codice molto grandi e applicazioni a prestazioni elevate. Nelle comunità di programmazione, i meriti relativi della tipizzazione dinamica nei confronti di quella statica sono dibattuti animatamente da lungo tempo. Molti punti di confronto sono abbastanza soggettivi. Non tratteremo tutte le argomentazioni in questa sede, ma offriremo alcune osservazioni degne di considerazione.

Ottimizzare le prestazioni di un linguaggio dinamico è più difficile che per un linguaggio statico. In un linguaggio statico, è possibile sfruttare le informazioni di tipo per decidere come ottimizzare. In un linguaggio dinamico, la scarsa disponibilità di questa sorta di informazioni rende le scelte di ottimizzazione più difficili. Sebbene i recenti progressi nella ottimizzazione di linguaggi dinamici siano promettenti, questi linguaggi rimangono comunque indietro rispetto allo stato dell’arte per i linguaggi statici. Quindi, se avete bisogno di prestazioni molto elevate, i linguaggi statici sono probabilmente una scelta più sicura.

I linguaggi statici possono anche portare benefici al processo di sviluppo. Alcune caratteristiche degli IDE come l’autocompletamento (a volte chiamato code sense) sono più facili da implementare per i linguaggi statici, sempre a causa delle informazioni di tipo aggiuntive disponibili. Le informazioni di tipo più esplicite nel codice statico promuovono una migliore “auto-documentazione”, che può essere importante per la comunicazione delle intenzioni tra gli sviluppatori, in particolar modo durante l’evoluzione di un progetto.

Quando usate un linguaggio statico, dovete pensare più spesso a scegliere i tipi appropriati, cosa che vi obbliga a pesare più attentamente le scelte di progettazione. Sebbene questo possa rallentare le decisioni di progettazione quotidiane, dal ragionamento sui tipi in un’applicazione può risultare una progettazione più coerente a lungo termine.

Un altro piccolo vantaggio dei linguaggi statici è il controllo aggiuntivo effettuato dal compilatore. Riteniamo che questo vantaggio sia spesso esageratamente lodato, in quanto gli errori che coinvolgono tipi male assortiti sono una piccola frazione degli errori a tempo di esecuzione che si vedono di solito. Il compilatore non è in grado di trovare gli errori di logica, che sono di gran lunga più significativi. Solo una serie di test completa e automatica può trovare gli errori di logica. Per i linguaggi dinamicamente tipati, i test devono anche coprire i possibili errori di tipo. Se provenite da un linguaggio dinamicamente tipato, potreste scoprire che le vostre serie di test sono un po’ più piccole, ma non tanto più piccole.

Molti sviluppatori che considerano i linguaggi statici troppo prolissi accusano spesso la tipizzazione statica per la prolissità mentre il problema reale è la mancanza della inferenza di tipo. Nell’inferenza di tipo, il compilatore ricava il tipo dei valori sulla base del contesto. Per esempio, il compilatore riconoscerà che x = 1 + 3 significa che x deve essere un intero. L’inferenza di tipo riduce significativamente la prolissità, rendendo il codice più simile a quello scritto in un linguaggio dinamico.

Abbiamo lavorato molte volte sia con linguaggi statici che con linguaggi dinamici. Abbiamo trovato interessanti entrambi i tipi di linguaggio, per ragioni differenti. Crediamo che il moderno sviluppatore di software debba padroneggiare un’ampia varietà di linguaggi e strumenti. A volte un linguaggio dinamico sarà lo strumento giusto per il vostro lavoro; altre volte, un linguaggio statico come Scala è esattamente quello che vi serve.

Una introduzione a Scala

Scala è un linguaggio che si rivolge ai bisogni principali dello sviluppatore moderno. È un linguaggio per la JVM staticamente tipato, a paradigma misto, con una sintassi concisa, elegante e flessibile, un sistema di tipi sofisticato e idiomi che promuovono la scalabilità dai piccoli programmi interpretati fino ad applicazioni sofisticate di grandi dimensioni. Questa è una definizione notevole, quindi esaminiamo ognuna di queste idee nel dettaglio.

Staticamente tipato

Come abbiamo detto nella sezione precedente, un linguaggio staticamente tipato lega il tipo a una variabile per il tempo di vita di quella variabile. Al contrario, i linguaggi dinamicamente tipati legano il tipo all’effettivo valore riferito da una variabile, permettendo quindi al tipo di una variabile di cambiare insieme al valore a cui fa riferimento.

Nel gruppo di nuovi linguaggi per la JVM, Scala è uno dei pochi a essere staticamente tipato e tra questi è quello più noto.

Paradigma misto – programmazione orientata agli oggetti

Scala supporta appieno la programmazione orientata agli oggetti (OOP). Scala migliora il supporto OOP di Java con l’aggiunta dei tratti, un modo pulito di implementare le classi usando la composizione dei mixin. I tratti di Scala funzionano in modo molto simile ai moduli di Ruby. Se siete programmatori Java, potete pensare ai tratti come all’unificazione delle interfacce con la loro implementazione.

In Scala, ogni cosa è davvero un oggetto. Scala non ha tipi primitivi come Java. Invece, tutti i tipi numerici sono veri oggetti. Tuttavia, per ottimizzare le prestazioni, Scala usa i tipi primitivi della macchina virtuale sottostante ogni volta che è possibile. In più, Scala non supporta membri “statici” o a livello di classe per i tipi, dato che non sono effettivamente associati a un’istanza. Invece, Scala supporta un costrutto di oggetto singleton per i casi in cui è necessaria una sola istanza di un tipo.

Paradigma misto – programmazione funzionale

Scala supporta appieno la programmazione funzionale (FP). La FP è un paradigma di programmazione più vecchio della OOP, ma è rimasta custodita nelle torri d’avorio delle università fino a poco tempo fa. L’interesse per la FP sta aumentando a causa del modo in cui semplifica certi problemi di progettazione, in particolare quelli legati alla concorrenza. I linguaggi funzionali “puri” non permettono alcuno stato mutabile, evitando di conseguenza il bisogno di sincronizzazione sull’accesso condiviso allo stato mutabile. Invece, i programmi scritti in linguaggi puramente funzionali comunicano scambiando messaggi tra processi autonomi e concorrenti. Scala supporta questo modello con la sua libreria di attori, ma consente di usare variabili mutabili e immutabili.

Le funzioni sono cittadini “di prima classe” nella FP, nel senso che possono essere assegnate a variabili, passate ad altre funzioni, &c., esattamente come gli altri valori. Questa caratteristica promuove la composizione di comportamenti avanzati usando operazioni primitive. Dato che Scala aderisce al principio per cui ogni cosa è un oggetto, in Scala anche le funzioni sono oggetti.

Scala offre anche le chiusure, una caratteristica che i linguaggi dinamici come Python e Ruby hanno adottato dal mondo della programmazione funzionale e che è tristemente assente nelle versioni recenti di Java. Le chiusure sono funzioni che fanno riferimento a variabili contenute nell’ambito che racchiude la definizione di funzione; le variabili non vengono passate come argomenti né definite come variabili locali all’interno della funzione. Una chiusura si “chiude attorno” a questi riferimenti, in modo che l’invocazione della funzione possa tranquillamente fare riferimento alle variabili anche quando le variabili sono uscite dall’ambito di visibilità! Le chiusure sono un’astrazione così potente da venire usate per implementare sistemi a oggetti e strutture di controllo fondamentali.

Un linguaggio per la JVM e per .NET

Sebbene Scala sia principalmente conosciuto come linguaggio per la JVM, nel senso che Scala genera bytecode per la JVM, una versione .NET di Scala che genera bytecode per il CLR è in fase di sviluppo. Quando parliamo della “macchina virtuale” sottostante di solito ci riferiamo alla JVM, ma la maggior parte di quello che diremo si applica ugualmente al CLR. Quando discutiamo i dettagli specifici per la JVM, essi possono venire generalizzati alla versione .NET, a parte alcuni casi che verranno esplicitamente indicati.

Il compilatore Scala usa tecniche ingegnose per correlare le estensioni di Scala a idiomi validi in bytecode. Da Scala potete facilmente invocare il bytecode creato a partire da codice sorgente Java (per la JVM) o C# (per .NET). Specularmente, potete invocare codice Scala da Java, C#, &c. Il fatto che Scala funzioni sulla JVM e sul CLR permette agli sviluppatori di sfruttare le librerie disponibili e interoperare con altri linguaggi ospitati su quelle macchine virtuali.

Una sintassi concisa, elegante e flessibile

La sintassi Java può essere prolissa. Scala usa un certo numero di tecniche per minimizzare la sintassi superflua, rendendo il codice Scala tanto conciso quanto il codice scritto nella maggior parte dei linguaggi dinamicamente tipati. L’inferenza di tipo minimizza il bisogno di esplicite informazioni di tipo in molti contesti. Le dichiarazioni di tipo e di funzione sono molto concise.

Scala permette ai nomi di funzione di includere caratteri non alfanumerici. Combinata con un pizzico di zucchero sintattico, questa caratteristica permette all’utente di definire metodi che sembrano operatori e che si comportano come tali. Come risultato, le librerie esterne al nucleo del linguaggio possono sembrare “native” a chi le usa.

Un sistema di tipi sofisticato

Scala estende il sistema di tipi di Java con generici più flessibili e un numero di costrutti di tipo più avanzati. Il sistema di tipi può incutere timore all’inizio, ma per la maggior parte del tempo non avrete bisogno di preoccuparvi dei costrutti avanzati. L’inferenza di tipo aiuta a ricavare automaticamente i tipi nelle firme dei metodi e delle funzioni, in modo che l’utente non debba fornire manualmente informazioni di tipo banali. Quando ne avete bisogno, comunque, le caratteristiche di tipo avanzate vi forniscono una maggiore flessibilità per risolvere problemi di progettazione in modo type-safe.

Scalabilità – architetture

Scala è progettato per scalare da piccoli programmi interpretati a grandi applicazioni distribuite. Scala fornisce quattro meccanismi linguistici che promuovono la composizione scalabile dei sistemi: 1) tipi espliciti per la classe corrente (chiamati self-type); 2) generici e membri tipo astratti; 3) classi annidate; e 4) composizione di mixin tramite tratti.

Nessun altro linguaggio fornisce tutti questi meccanismi, che insieme consentono di costruire applicazioni in maniera concisa e type-safe attraverso “componenti” riusabili. Come vedremo, molti modelli di progettazione e tecniche architetturali come l’iniezione di dipendenza (dependency injection) sono facili da implementare in Scala, evitando quel codice ridondante o quei lunghi file XML di configurazione che rendono fastidioso lo sviluppo in Java.

Scalabilità – prestazioni

Dato che il codice Scala può essere eseguito sulla JVM e sul CLR, i programmi traggono beneficio da tutte le ottimizzazioni di prestazione presenti in quelle macchine virtuali e da tutti gli strumenti di terze parti che sono di ausilio a efficienza e scalabilità, come analizzatori di codice, librerie di cache distribuita, meccanismi di clustering, &c. Se fate affidamento sulle prestazioni di Java e C#, potete fare affidamento sulle prestazioni di Scala. Naturalmente, alcuni costrutti particolari del linguaggio e alcune parti della libreria potrebbero funzionare significativamente meglio o peggio delle opzioni alternative in altri linguaggi. Come sempre, dovreste analizzare il vostro codice e ottimizzarlo quando è necessario.

Potrebbe sembrare che OOP e FP siano incompatibili tra loro. In effetti, una delle filosofie di progettazione di Scala è che OOP e FP siano più sinergiche che opposte. Le caratteristiche di un approccio possono migliorare l’altro.

Nella FP le funzioni non hanno effetti collaterali e le variabili sono immutabili, mentre nella OOP lo stato mutabile e gli effetti collaterali sono comuni, persino incoraggiati. Scala vi permette di scegliere l’approccio più adatto ai vostri problemi di progettazione. La programmazione funzionale è particolarmente utile per la concorrenza, dato che elimina il bisogno di sincronizzare l’accesso allo stato mutabile. Tuttavia, la FP “pura” può risultare restrittiva. Alcuni problemi di progettazione sono più facili da risolvere con gli oggetti mutabili.

Il nome Scala è una contrazione delle parole scalable language (linguaggio scalabile). Sebbene questo suggerisca che la pronuncia debba essere scale-ah, i creatori di Scala in realtà lo pronunciano scah-lah, proprio come la parola italiana. Le due “a” vengono pronunciate allo stesso modo.

Scala è stato concepito da Martin Odersky nel 2001. Martin è un professore della School of Computer and Communication Sciences alla Ecole Polytechnique Fédérale de Lausanne (EPFL). Ha trascorso i suoi anni di dottorato lavorando nel gruppo guidato da Niklaus Wirth, famoso per il linguaggio Pascal. Martin ha lavorato su Pizza, un primo linguaggio funzionale per la JVM. Più tardi ha lavorato su GJ, un prototipo di quello che è poi diventato il progetto Generics in Java, con Philip Walder, famoso per il linguaggio Haskell. Martin è stato assunto da Sun Microsystems per produrre l’implementazione di riferimento di javac, il compilatore Java oggi incluso nel Java Development Kit (JDK).

La formazione e l’esperienza di Martin Odersky sono evidenti nel linguaggio. Man mano che imparerete Scala, finirete per capire come esso sia il prodotto di decisioni di progetto attentamente ponderate che sfruttano lo stato dell’arte nella teoria dei tipi, nella OOP e nella FP. L’esperienza di Martin con la JVM è evidente nell’eleganza con cui Scala è integrato con quella piattaforma. La sintesi creata tra OOP e FP è una soluzione eccellente che contiene “il meglio dei due mondi”.

Le seduzioni di Scala

Oggi la nostra industria è fortunata ad avere un’ampia varietà di scelta per i linguaggi di programmazione. La potenza, la flessibilità e l’eleganza dei linguaggi dinamicamente tipati li ha resi di nuovo popolari. Tuttavia, la ricchezza delle librerie Java e .NET e le prestazioni di JVM e CLR soddisfano molte delle necessità pratiche di progetti industriali e di rete.

Scala è attraente perché sembra un linguaggio di scripting dinamicamente tipato grazie alla sua sintassi concisa e all’inferenza di tipo. Però Scala vi offre tutti i vantaggi della tipizzazione statica, di un modello a oggetti moderno, della programmazione funzionale e di un sistema di tipi avanzato. Questi strumenti vi permettono di costruire applicazioni scalabili e modulari che possono riutilizzare API legacy in Java e .NET e sfruttare le prestazioni di JVM e CLR.

Scala è un linguaggio per sviluppatori professionali. Paragonato a linguaggi come Java e Ruby, Scala è un linguaggio più difficile da padroneggiare, perché richiede competenze di OOP, FP e tipizzazione statica per essere usato nella maniera più efficace. La relativa semplicità dei linguaggi dinamicamente tipati è allettante, ma questa semplicità può essere ingannevole. In un linguaggio dinamicamente tipato, è spesso necessario usare tecniche di metaprogrammazione per realizzare progetti avanzati. Sebbene la metaprogrammazione sia potente, usarla bene richiede esperienza e il codice risultante tende a essere difficile da capire, mantenere e correggere. In Scala, molti degli stessi obiettivi di progettazione possono essere raggiunti in maniera type-safe sfruttando il suo sistema di tipi e la composizione dei mixin tramite i tratti.

Crediamo che lo sforzo aggiuntivo richiesto quotidianamente per usare Scala vi aiuterà a riflettere attentamente sui vostri progetti. Nel tempo, questa disciplina produrrà applicazioni coerenti, modulari e mantenibili. Fortunatamente, non dovete usare sempre tutte le caratteristiche più sofisticate di Scala. Buona parte del vostro codice sarà semplice e chiara come il codice scritto nel vostro linguaggio dinamicamente tipato preferito.

Una strategia alternativa è quella di combinare diversi linguaggi più semplici, come per esempio Java per il codice orientato agli oggetti ed Erlang per il codice funzionale e concorrente. Una decomposizione simile può funzionare, ma solo se il vostro sistema si può scindere in più parti separate in maniera pulita e se il vostro gruppo è in grado di gestire un ambiente eterogeneo. Scala è una soluzione attraente quando un singolo linguaggio “tutto compreso” è preferibile. Detto questo, il codice Scala può tranquillamente coesistere con altri linguaggi, in particolare sulla JVM o in .NET.

Installare Scala

Per cominciare il più velocemente possibile, questa sezione descrive come installare gli strumenti a riga di comando di Scala, che sono tutto ciò che vi serve per lavorare con gli esempi di questo libro. Per i dettagli su come usare Scala in vari editor e IDE fate riferimento alla sezione Integrazione con gli IDE nel capitolo 14. Gli esempi usati in questo libro sono stati scritti e compilati usando sia la versione 2.7.5 di Scala, l’ultima rilasciata al momento della scrittura, sia gli “assemblaggi notturni” di Scala 2.8.0, che potrebbe essere già stata rilasciata nel momento in cui state leggendo queste pagine.

La versione 2.8 introduce molte nuove caratteristiche che sottolineeremo nel corso del libro.

In questo libro lavoreremo con la versione di Scala per la JVM. Come prima cosa, dovete avere installato Java 1.4 o superiore (viene raccomandata la versione 1.5 o superiore). Se avete bisogno di installare Java, andate alla pagina web http://www.java.com/en/download/manual.jsp e seguite le istruzioni per installare Java sul vostro computer.

Il sito web ufficiale di Scala è http://www.scala-lang.org. Per installare Scala, andate alla pagina http://www.scala-lang.org/downloads, scaricate il programma di installazione per il vostro ambiente e seguite le istruzioni su quella pagina.

Il programma di installazione indipendente dalla piattaforma e più semplice da usare è il pacchetto IzPack. Scaricate il file JAR di Scala, scala-2.7.5.final-installer.jar oppure scala-2.8.0.N-installer.jar, dove N indica il numero di revisione della versione 2.8.0. Tramite una finestra di terminale posizionatevi nella directory dove avete scaricato il file e installate Scala con il comando java. Supponendo che abbiate scaricato scala-2.8.0.final-installer.jar, eseguite il comando seguente, che vi guiderà attraverso il processo di installazione.

java -jar scala-2.8.0.final-installer.jar

Su Mac OS X la strada più facile per arrivare a una installazione funzionante di Scala è quella di usare MacPorts. Seguite le istruzioni di installazione su http://www.macports.org, poi eseguite sudo port install scala. Potrete cominciare a lavorare entro pochi minuti.

In tutto il libro useremo il simbolo scala-home per fare riferimento alla directory “radice” della vostra installazione di Scala.

Sui sistemi Unix, Linux e Mac OS X dovrete eseguire questo comando come utente root o usando il comando sudo se volete installare Scala in una directory di sistema, in modo che, per esempio, sia scala-home = /usr/local/scala-2.8.0.final.

Come alternativa, sui sistemi Unix potete scaricare il file compresso tar (per esempio, scala-2.8.0.final.tgz) o zip (per esempio, scala-2.8.0.final.zip) ed estrarne i contenuti in una directory di vostra scelta. Dopodiché aggiungete la sottodirectory scala-home/bin alla vostra variabile d’ambiente PATH. Per esempio, se avete installato Scala in /usr/local/scala-2.8.0.final, allora aggiungete /usr/local/scala-2.8.0.final/bin alla vostra variabile d’ambiente PATH.

Per collaudare la vostra installazione, lanciate il comando seguente sulla riga di comando:

scala -version

Impareremo di più sullo strumento a riga di comando scala più avanti. Dovreste ottenere un messaggio simile al seguente:

Scala code runner version 2.8.0.final -- Copyright 2002-2009, LAMP/EPFL

Naturalmente, il numero che vedrete sarà differente se avete installato una versione differente. D’ora in poi, quando mostreremo messaggi a riga di comando che contengono il numero di versione, li mostreremo come version 2.8.0.final.

Congratulazioni, avete installato Scala! Se ottenete un messaggio di errore del tipo scala: command not found assicuratevi che la vostra variabile d’ambiente PATH sia opportunamente impostata per includere la directory bin corretta.

Le versioni 2.7.X di Scala e quelle precedenti sono compatibili con il JDK 1.4 o successivo. La versione 2.8 di Scala rinuncia alla compatibilità con la versione 1.4. Notate che Scala usa molte classi del JDK, per esempio la classe String. Su .NET Scala usa le classi corrispondenti per .NET.

Potete anche scaricare la documentazione delle API e i sorgenti di Scala dalla stessa pagina che avete usato per scaricare Scala.

Per maggiori informazioni

Man mano che esplorate Scala, troverete altre utili risorse che sono disponibili su http://scala-lang.org. Troverete link a strumenti di supporto per lo sviluppo e a librerie, a tutorial, alla specifica del linguaggio [ScalaSpec2009] e ad articoli scientifici che descrivono alcune caratteristiche del linguaggio.

La documentazione per gli strumenti e per le API di Scala è particolarmente utile. Potete navigare la documentazione delle API all’indirizzo http://www.scala-lang.org/docu/files/api/index.html. Questa documentazione è stata generata usando lo strumento scaladoc, analogo allo strumento javadoc di Java. Leggete la sezione Lo strumento scaladoc a riga di comando nel capitolo 14 per maggiori informazioni.

Potete anche scaricare un file compresso contenente la documentazione delle API per leggerla in locale usando il link appropriato sulla pagina http://www.scala-lang.org/downloads, oppure potete installare la documentazione usando lo strumento di impacchettamento sbaz nel modo seguente.

sbaz install scala-devel-docs

sbaz viene installato nella stessa directory bin dove si trovano gli strumenti a riga di comando scala e scalac. La documentazione installata comprende anche dettagli su tutti gli strumenti inclusi in Scala (compreso sbaz) e diversi esempi di codice. Per maggiori informazioni sugli strumenti a riga di comando di Scala e su ulteriori risorse, si veda il capitolo 14.

Un assaggio di Scala

È il momento di stuzzicare il vostro appetito con un po’ di vero codice Scala. Negli esempi che seguono descriveremo i particolari sufficienti a farvi capire cosa sta succedendo, per darvi un’idea di come sono fatti i programmi Scala. Esploreremo i dettagli delle caratteristiche del linguaggio nei prossimi capitoli.

Potete eseguire il nostro primo esempio in due modi: interattivamente o come uno “script”.

Cominciamo con la modalità interattiva. Lanciate l’interprete Scala digitando scala e premendo il tasto di invio sulla vostra riga di comando. Vedrete la seguente uscita. (Alcuni numeri di versione potrebbero variare.)

Welcome to Scala version 2.8.0.final (Java …).
Type in expressions to have them evaluated.
Type :help for more information.

scala>

L’ultima riga è il prompt che attende le vostre istruzioni in ingresso. La modalità interattiva di scala è molto conveniente per fare esperimenti (si veda la sezione Lo strumento scala a riga di comando nel capitolo 14 per i dettagli). Un interprete interattivo come questo viene chiamato REPL: Read-Evaluate-Print Loop (letteralmente, ciclo di lettura-valutazione-stampa).

Digitate le due righe di codice seguenti.

val book = "Programmare in Scala"
println(book)

Gli effettivi ingressi e uscite dovrebbero somigliare a quanto segue.

scala> val book = "Programmare in Scala"
book: java.lang.String = Programmare in Scala

scala> println(book)
Programmare in Scala

scala>

La prima riga usa la parola chiave val per dichiarare una variabile a sola lettura chiamata book. Notate che il risultato restituito dall’interprete vi mostra il tipo e il valore di book. Questo può essere molto comodo per capire dichiarazioni complesse. La seconda riga stampa il valore di book, che è “Programmare in Scala”.

Fare esperimenti con il comando scala nella modalità interattiva (REPL) è un ottimo modo per imparare i dettagli di Scala.

Molti degli esempi in questo libro possono essere eseguiti nell’interprete in questo modo. Tuttavia, spesso è più conveniente usare la seconda opzione che abbiamo menzionato, cioè scrivere gli script Scala in un editor di testo o in un IDE ed eseguirli con lo stesso comando scala. Faremo così per gli esempi rimanenti in questo capitolo.

Dal vostro editor di testo preferito, salvate il codice Scala dell’esempio seguente in un file chiamato upper1-script.scala in una directory di vostra scelta.

// esempi/cap-1/upper1-script.scala

class Upper {
  def upper(strings: String*): Seq[String] = {
    strings.map((s: String) => s.toUpperCase())
  }
}

val up = new Upper
Console.println(up.upper("Un", "Primo", "Programma", "Scala"))

Questo programma Scala converte stringhe in maiuscolo.

A proposito, quello sulla prima riga è un commento (con il nome del file sorgente che contiene l’esempio di codice). Per i commenti, Scala segue le stesse convenzioni di Java, C#, C++, &c. Un // commento prosegue fino alla fine di una riga, mentre un /* commento */ può occupare più righe.

Per eseguire questo programma, aprite una finestra di terminale, entrate nella stessa directory e lanciate il comando seguente.

scala upper1-script.scala

Il file viene interpretato, nel senso che viene compilato ed eseguito in un unico passo. Dovreste ottenere il seguente risultato:

Array(UN, PRIMO, PROGRAMMA, SCALA)

In questo esempio, il metodo upper della classe Upper (nessun gioco di parole intenzionale) converte le stringhe in ingresso in maiuscolo e le restituisce in un array. L’ultima riga dell’esempio converte quattro stringhe e stampa l’array risultante.

Esaminiamo il codice nel dettaglio, così possiamo cominciare a imparare la sintassi di Scala. Ci sono molti dettagli in sole sei righe di codice! Qui illustreremo le idee generali. Tutte le idee usate in questo esempio verranno spiegate in maniera più esauriente nelle prossime sezioni del libro.

In questo esempio, la classe Upper comincia con la parola chiave class. Il corpo della classe si trova all’interno della coppia più esterna di parentesi graffe {…}.

La definizione del metodo upper comincia nella seconda riga con la parola chiave def seguita dal nome del metodo e da una lista di argomenti, dal tipo di ritorno del metodo, da un segno di uguale “=” e poi dal corpo del metodo.

La lista di argomenti tra parentesi è in realtà una lista di argomenti a lunghezza variabile composta da stringhe, come indicato dal tipo String* che segue i due punti. Questo significa che potete passare tante stringhe separate da una virgola quante ne volete (compresa una lista vuota). Queste stringhe vengono memorizzate in un parametro chiamato strings. All’interno del metodo, strings è in realtà un’istanza di Array.

Quando il codice contiene un’informazione di tipo esplicita per le variabili, queste annotazioni di tipo seguono i due punti dopo il nome dell’elemento (cioè secondo una sintassi simile al Pascal). Perché Scala non segue le convenzioni Java? Ricordatevi che l’informazione di tipo è spesso inferita in Scala (a differenza di Java), quindi non dobbiamo mostrare sempre le annotazioni di tipo in maniera esplicita. Rispetto alla convenzione tipo elemento di Java, la convenzione elemento: tipo è più facile da analizzare in maniera univoca per il compilatore quando omettete i due punti e l’annotazione di tipo scrivendo semplicemente elemento.

Il tipo di ritorno del metodo compare dopo la lista degli argomenti. In questo caso, il tipo di ritorno è Seq[String], dove Seq (che sta per “sequenza”) è un tipo particolare di collezione. Questo è un tipo parametrico (simile a un tipo generico in Java), in questo caso parametrizzato con String. Notate che Scala usa le parentesi quadre […] per i tipi parametrici, mentre Java usa le parentesi angolari <…>.

Scala consente di usare parentesi angolari nei nomi dei metodi; per esempio, è pratica comune chiamare un metodo “minore di” con il nome <. Quindi, in modo da evitare ambiguità, Scala usa le parentesi quadre per i tipi parametrici perché non possono essere usate nel nome dei metodi. Scala non segue la convenzione Java per le parentesi angolari perché consente di usare < e > nei nomi dei metodi.

Il corpo del metodo upper si trova dopo il segno di uguale “=”. Perché un segno di uguale? Perché non semplicemente una coppia di parentesi graffe {…} come in Java? Dato che i punti e virgola, i tipi di ritorno delle funzioni, le liste di argomenti per i metodi e persino le parentesi graffe vengono talvolta omesse, usare un segno di uguale evita diverse possibili ambiguità nella sintassi del linguaggio. Il segno di uguale ci ricorda anche che persino le funzioni sono valori in Scala, una caratteristica coerente con il supporto offerto da Scala per la programmazione funzionale, descritto in maggiore dettaglio nel capitolo 8.

Il corpo del metodo invoca il metodo map sull’array strings passandogli un letterale funzione come argomento. I letterali funzione sono funzioni “anonime”, simili alle funzioni lambda, alle chiusure, ai blocchi, o alle proc che si possono trovare in altri linguaggi. In Java, qui dovreste usare una classe annidata anonima che implementa un metodo definito da un’interfaccia, &c.

In questo caso, abbiamo passato il seguente letterale funzione.

(s: String) => s.toUpperCase()

Questo letterale prende una lista di argomenti con un singolo argomento di tipo String chiamato s. Il corpo del letterale funzione si trova dopo la “freccia” => e invoca toUpperCase() su s. Il letterale funzione restituisce il risultato di questa invocazione. In Scala, l’ultima espressione contenuta in una funzione è il valore di ritorno, sebbene possiate anche avere istruzioni return in altri punti. La parola chiave return è opzionale e viene raramente usata, a parte quando si ritorna da un punto che si trova in mezzo a un blocco (per esempio in una istruzione if).

Il valore dell’ultima espressione è il valore di ritorno predefinito di una funzione. L’istruzione return non è obbligatoria.

Quindi, map passa ogni stringa contenuta in strings al letterale funzione e assembla una nuova collezione con i risultati restituiti dal letterale funzione.

Per provare il codice, creiamo una nuova istanza di Upper e la assegniamo a una variabile chiamata up. Come in Java, C# e linguaggi simili, la sintassi new Upper crea una nuova istanza. La variabile up è dichiarata come un “valore” a sola lettura usando la parola chiave val.

Infine, invochiamo il metodo upper su una lista di stringhe e stampiamo il risultato con Console.println(…), che è equivalente al System.out.println(…) di Java.

In realtà possiamo semplificare ulteriormente il nostro programma. Considerate questa versione semplificata del programma.

// esempi/cap-1/upper2-script.scala

object Upper {
  def upper(strings: String*) = strings.map(_.toUpperCase())
}

println(Upper.upper("Un", "Primo", "Programma", "Scala"))

Questo codice fa esattamente la stessa cosa, ma usando un terzo di caratteri in meno.

Nella prima riga Upper ora è dichiarata come un object, che è un singleton. Stiamo dichiarando una classe, ma l’interprete Scala creerà una sola istanza di Upper. (Non potete scrivere new Upper, per esempio.) Scala usa gli object per situazioni in cui altri linguaggi userebbero membri “a livello di classe”, come i membri static in Java. In questo caso non abbiamo bisogno di più di una istanza, quindi un singleton va bene.

Perché Scala non supporta i membri statici? Dato che ogni cosa è un oggetto in Scala, il costrutto object è coerente con questa politica. I metodi e i campi statici in Java non sono legati ad alcuna istanza.

Notate che questo codice è completamente thread-safe. Non stiamo dichiarando alcuna variabile che potrebbe causare problemi di sicurezza dei thread; anche i metodi di API che usiamo sono thread-safe. Di conseguenza, non ci servono molteplici istanze. Ci basta un singolo object.

Anche l’implementazione di upper nella seconda riga è più semplice. Di solito Scala è in grado di inferire il tipo di ritorno del metodo (ma non i tipi degli argomenti del metodo), quindi evitiamo di dichiararlo esplicitamente. In più, dato che il corpo del metodo contiene una sola espressione, omettiamo le parentesi e collochiamo l’intera definizione di metodo su una riga. Il segno di uguale prima del corpo del metodo indica al compilatore, così come al lettore umano, il punto in cui comincia il corpo del metodo.

Abbiamo anche sfruttato un’abbreviazione per il letterale funzione. In precedenza lo avevamo scritto in questo modo.

(s: String) => s.toUpperCase()

Possiamo abbreviarlo nella seguente espressione.

_.toUpperCase()

Dato che map accetta un unico argomento (una funzione) possiamo usare l’indicatore “segnaposto” _ invece di un parametro con nome. L’indicatore _ agisce come una variabile anonima a cui viene assegnata ogni stringa prima di invocare toUpperCase. Notate anche che il tipo String viene inferito. Come vedremo, Scala usa _ come una “wildcard” in diversi contesti.

Potete anche usare questa sintassi abbreviata in alcuni letterali funzione più complessi, come vedremo nel capitolo 3.

Nell’ultima riga, l’uso di un object al posto di una classe semplifica il codice. Invece di creare un’istanza con new Upper, possiamo limitarci a invocare il metodo upper direttamente sull’oggetto Upper (notate come questo somigli alla sintassi che usereste per invocare un metodo statico di una classe Java).

Infine, Scala importa automaticamente molti metodi di I/O come println, quindi non abbiamo bisogno di invocare Console.println(); possiamo semplicemente usare println. (Si veda la sezione L’oggetto predefinito nel capitolo 7 per i dettagli sui tipi e sui metodi che vengono importati o definiti automaticamente.)

Effettuiamo un ultimo refactoring: convertiamo lo script in uno strumento compilato a riga di comando.

// esempi/cap-1/upper3.scala

object Upper {
  def main(args: Array[String]) = {
    args.map(_.toUpperCase()).foreach(printf("%s ",_))
    println("")
  }
}

Ora il metodo upper è stato rinominato main. Dato che Upper è un object, questo metodo main funziona esattamente come un metodo static main di una classe Java. È il punto di ingresso per l’applicazione Upper.

In Scala, main deve essere un metodo di un object. (In Java, main deve essere un metodo static di una classe.) Gli argomenti sulla riga di comando vengono passati a main in un array di stringhe, per esempio args: Array[String].

La prima riga nel metodo main usa la stessa notazione abbreviata per map che abbiamo appena esaminato.

args.map(_.toUpperCase())…

L’invocazione di map restituisce una nuova collezione. Iteriamo attraverso di essa con foreach, usando ancora il segnaposto _ in un nuovo letterale funzione che passiamo a foreach. In questo caso, ogni stringa contenuta nella collezione viene passata come argomento a printf.

…foreach(printf("%s ",_))

Per essere chiari, questi due usi di _ sono completamente indipendenti tra loro. La concatenazione dei metodi e le abbreviazioni dei letterali funzione, come in questo esempio, possono richiedere un po’ di abitudine per essere usati in maniera efficace, ma una volta che vi trovate a vostro agio producono codice molto leggibile con un impiego minimo di variabili temporanee.

L’ultima riga del metodo main aggiunge un ritorno a capo conclusivo all’uscita.

Questa volta, dovete prima compilare il codice in un file .class per la JVM usando scalac.

scalac upper3.scala

Ora dovreste avere un file chiamato Upper.class, esattamente come se aveste appena compilato una classe Java.

Potreste aver notato che il compilatore non si è lamentato quando ha visto che il file è chiamato upper3.scala e l’object è chiamato Upper. A differenza di Java, il nome del file non deve corrispondere al nome del tipo dichiarato come public. (Esploreremo le regole di visibilità nella sezione Regole di visibilità del capitolo 5.) In effetti, a differenza di Java, potete mettere tutti i tipi pubblici che volete in un singolo file. In più, la directory in cui si trova un file non deve corrispondere alla dichiarazione del package. Tuttavia, potete certamente seguire le convenzioni Java se volete farlo.

Ora potete eseguire questo comando su qualunque lista di stringhe. Ecco un esempio.

scala -cp . Upper Ciao Mondo!

L’opzione -cp . aggiunge la directory corrente al class path, il percorso di ricerca delle classi. Dovreste ottenere la seguente uscita.

CIAO MONDO!

Abbiamo dunque soddisfatto il requisito per cui un libro che parla di un linguaggio di programmazione deve cominciare con un programma “ciao mondo”.

Un assaggio di concorrenza

Ci sono molte ragioni per lasciarsi sedurre da Scala. Una ragione è la API Actors inclusa nella libreria Scala, basata sul robusto modello di concorrenza Actors implementato in Erlang [Haller2007]. In questa sezione vi presenteremo un esempio per stuzzicarvi l’appetito.

Il modello di concorrenza ad attori [Agha1987] si basa su entità software indipendenti chiamate attori che non condividono tra loro alcuna informazione di stato e comunicano scambiandosi messaggi. Eliminando il bisogno di sincronizzare l’accesso a uno stato mutabile condiviso, è molto più facile scrivere applicazioni concorrenti robuste.

In questo esempio, alcune istanze di una gerarchia di forme geometriche vengono inviate a un attore per essere disegnate su uno schermo. Immaginate uno scenario in cui una rendering farm genera le scene di un’animazione. Man mano che il rendering di una scena viene completato, le forme “primitive” che fanno parte della scena vengono inviate a un attore di un sottosistema di visualizzazione.

Per cominciare, definiamo una gerarchia di forme a partire dalla classe Shape.

// esempi/cap-1/shapes.scala

package shapes {
  class Point(val x: Double, val y: Double) {
    override def toString() = "Point(" + x + "," + y + ")"
  }

  abstract class Shape() {
    def draw(): Unit
  }

  class Circle(val center: Point, val radius: Double) extends Shape {
    def draw() = println("Circle.draw: " + this)
    override def toString() = "Circle(" + center + "," + radius + ")"
  }

  class Rectangle(val lowerLeft: Point, val height: Double, val width: Double)
        extends Shape {
    def draw() = println("Rectangle.draw: " + this)
    override def toString() =
      "Rectangle(" + lowerLeft + "," + height + "," + width + ")"
  }

  class Triangle(val point1: Point, val point2: Point, val point3: Point)
        extends Shape {
    def draw() = println("Triangle.draw: " + this)
    override def toString() =
      "Triangle(" + point1 + "," + point2 + "," + point3 + ")"
  }
}

La gerarchia della classe Shape è definita in un package shapes. Potete dichiarare il package usando la sintassi Java, ma Scala supporta anche una sintassi simile a quella degli “spazi di nomi” in C#, dove l’intera dichiarazione è racchiusa tra parentesi graffe, come in questo caso. Tuttavia, essendo sia compatta che leggibile, la sintassi Java per la dichiarazione dei package viene usata molto più spesso.

La classe Point rappresenta un punto bidimensionale su un piano. Notate la lista di argomenti dopo il nome della classe. Quelli sono i parametri di un costruttore. In Scala, l’intero corpo della classe è il costruttore, quindi gli argomenti per il costruttore principale vanno elencati dopo il nome della classe e prima del corpo della classe. (Vedremo come definire costruttori ausiliari nella sezione Costruttori in Scala del capitolo 5.) Dato che abbiamo messo la parola chiave val prima di ogni dichiarazione di parametro, i parametri vengono automaticamente convertiti in campi a sola lettura con lo stesso nome associati a metodi pubblici di lettura che hanno lo stesso nome; cioè, quando create un’istanza della classe Point, per esempio punto, potete leggere i campi usando punto.x e punto.y. Se volete campi mutabili, allora usate la parola chiave var. Esploreremo le dichiarazioni di variabile e le parole chiave val e var nella sezione Dichiarazioni di variabile del capitolo 2.

Il corpo di Point definisce un metodo che è la ridefinizione del metodo toString comune in Java (come ToString in C#). Notate che Scala, come C#, richiede la parola chiave override ogni volta che ridefinite un metodo concreto. A differenza di C#, non dovete usare la parola chiave virtual sul metodo concreto originale. In effetti, non c’è alcuna parola chiave virtual in Scala. Come prima, omettiamo le parentesi graffe {…} attorno al corpo di toString() dato che contiene solo una singola espressione.

Shape è una classe astratta. Le classi astratte di Scala sono simili a quelle di Java e C#. Non possiamo istanziare una classe astratta, anche quando tutti i suoi campi e i suoi metodi sono concreti.

In questo caso, Shape dichiara un metodo astratto draw. Sappiamo che è astratto perché non ha un corpo. La parola chiave abstract sul metodo non è obbligatoria. I metodi astratti di Scala sono esattamente come i metodi astratti di Java e C#. (Si veda la sezione Ridefinire i membri di classi e tratti nel capitolo 6 per maggiori dettagli.)

Il metodo draw restituisce il tipo Unit, pressoché equivalente al tipo void dei linguaggi derivati dal C come Java &c. (Si veda la sezione La gerarchia di tipi di Scala nel capitolo 7 per maggiori dettagli.)

Circle è dichiarata come una sottoclasse concreta di Shape. Definisce il metodo draw in modo che stampi semplicemente un messaggio sulla console. Circle ridefinisce anche il metodo toString.

Anche Rectangle è una sottoclasse concreta di Shape che definisce draw e ridefinisce toString. Per semplicità, supporremo che il rettangolo non sia ruotato in relazione agli assi X e Y, quindi tutto ciò di cui abbiamo bisogno è un punto (il vertice in basso a sinistra andrà bene) insieme alle dimensioni di altezza e larghezza.

Triangle segue lo stesso schema. Prende tre istanze di Point come argomenti del proprio costruttore.

Tutti i metodi draw nelle classi Circle, Rectangle e Triangle usano this. Come in Java e C#, this è il modo in cui un’istanza fa riferimento a se stessa. In questo contesto, dove this è la parte destra di un’espressione che concatena stringhe (usando il segno più), this.toString viene invocato implicitamente.

Naturalmente, in un’applicazione reale non implementereste le operazioni di disegno nelle classi che fanno parte del “modello del dominio” in questo modo, dato che l’implementazione dipenderebbe da dettagli come la piattaforma del sistema operativo, le API grafiche, &c. Vedremo un approccio di progettazione migliore quando discuteremo i tratti nel capitolo 4.

Ora che abbiamo definito i nostri tipi di forme, torniamo agli attori. Definiamo un Actor che riceve “messaggi” che sono forme da disegnare.

// esempi/cap-1/shapes-actor.scala

package shapes {
  import scala.actors._
  import scala.actors.Actor._

  object ShapeDrawingActor extends Actor {
    def act() {
      loop {
        receive {
          case s: Shape => s.draw()
          case "exit"   => println("termino..."); exit
          case x: Any   => println("Errore: messaggio sconosciuto! " + x)
        }
      }
    }
  }
}

L’attore viene dichiarato come parte del package shapes. Subito dopo, troviamo due istruzioni di importazione.

La prima istruzione importa tutti i tipi dal package scala.actors. In Scala, il trattino basso _ è usato nel modo in cui l’asterisco * viene usato in Java.

Dato che * è un carattere valido per un nome di funzione, non può essere usato come wildcard in una istruzione import. Al suo posto, viene riservato _ per questo scopo.

Tutti i metodi e i campi pubblici di Actor vengono importati. Queste non sono importazioni statiche dal tipo Actor come sarebbero in Java. Piuttosto, metodi e campi sono importati da un object chiamato anch’esso Actor. La classe e l’oggetto hanno lo stesso nome, come vedremo nella sezione Oggetti associati del capitolo 6.

La classe ShapeDrawingActor che definisce il nostro attore è un object che estende Actor (il tipo, non l’oggetto). Il metodo act viene ridefinito per svolgere la particolare operazione dell’attore. Dato che act è un metodo astratto, non abbiamo bisogno di ridefinirlo esplicitamente con la parola chiave override. Il nostro attore aspetta i messaggi in arrivo in un ciclo infinito.

Durante ogni passo del ciclo viene invocato il metodo receive, che si blocca fino a quando non arriva un nuovo messaggio. Perché il codice che segue receive è racchiuso tra parentesi graffe ({…}) e non tra parentesi tonde ((…))? Impareremo più avanti che ci sono casi in cui questa sostituzione è permessa ed è piuttosto utile (si veda il capitolo 3). Per ora, vi basterà sapere che le espressioni all’interno delle parentesi costituiscono un singolo letterale funzione che viene passato a receive. Questo letterale funzione esegue un’operazione di pattern matching (corrispondenza di pattern) sull’istanza del messaggio per decidere come gestirlo. Grazie alle clausole case questa sembra una tipica istruzione switch di Java, e in effetti il suo comportamento è molto simile.

La prima clausola case effettua un confronto di tipo con il messaggio. (Non c’è alcuna variabile esplicita per l’istanza del messaggio nel codice, perché viene inferita.) Se il messaggio è di tipo Shape, la prima clausola case corrisponde. L’istanza del messaggio viene convertita in un’istanza di Shape e assegnata alla variabile s, sulla quale viene poi invocato il metodo draw.

Se il messaggio non è di tipo Shape, viene provata la seconda clausola case. Se il messaggio è la stringa "exit", l’attore stampa un messaggio e termina la propria esecuzione. Di solito bisognerebbe dare agli attori la possibilità di terminare normalmente!

L’ultima clausola case gestisce tutti gli altri messaggi, assumendo il ruolo di caso predefinito. L’attore riporta un errore e poi scarta il messaggio. Any è il genitore di tutti i tipi nella gerarchia di tipi di Scala, così come Object è il tipo radice in Java e in altri linguaggi. Quindi, questa clausola case corrisponderà a qualsiasi messaggio di qualsiasi tipo. Il pattern matching è avido; dobbiamo mettere questa clausola alla fine per evitare che consumi anche i messaggi che stiamo aspettando!

Ricordatevi che abbiamo dichiarato draw come un metodo astratto in Shape e abbiamo implementato draw nelle sottoclassi concrete. Quindi il codice nella prima istruzione case invoca un’operazione polimorfica.

Infine, ecco uno script che usa l’attore ShapeDrawingActor.

// esempi/cap-1/shapes-actor-script.scala

import shapes._

ShapeDrawingActor.start()

ShapeDrawingActor ! new Circle(new Point(0.0, 0.0), 1.0)
ShapeDrawingActor ! new Rectangle(new Point(0.0, 0.0), 2, 5)
ShapeDrawingActor ! new Triangle(new Point(0.0, 0.0),
                                 new Point(1.0, 0.0),
                                 new Point(0.0, 1.0))
ShapeDrawingActor ! 3.14159

ShapeDrawingActor ! "exit"

Le forme nel package shape vengono importate.

L’attore ShapeDrawingActor viene fatto partire e si mette in attesa di messaggi in arrivo. Per default, viene eseguito in un proprio thread (ci sono alternative che discuteremo nel capitolo 9).

Vengono inviati cinque messaggi all’attore, usando la sintassi attore ! messaggio. Il primo messaggio è un’istanza di Circle; l’attore “disegna” il cerchio. Il secondo messaggio è un oggetto Rectangle; l’attore “disegna” il rettangolo. In risposta al terzo messaggio, l’attore fa la stessa cosa per un triangolo. Il quarto messaggio è un numero Double approssimativamente uguale a π. Questo è un messaggio sconosciuto per l’attore, che quindi si limita a stampare un messaggio di errore. Il messaggio finale è la stringa "exit", che provoca la terminazione dell’attore.

Per provare l’esempio con gli attori, cominciate a compilare i primi due file. Potete scaricare i sorgenti dal sito di O’Reilly (si veda la sezione Ottenere gli esempi di codice nella Prefazione per i dettagli) o potete crearli voi stessi.

Usate il comando seguente per compilare i file.

scalac shapes.scala shapes-actor.scala

Sebbene il nome dei file sorgente e le directory non debbano corrispondere al contenuto dei file, noterete che i file di classe vengono generati in una directory shapes e che c’è un file di classe per ogni classe che abbiamo definito. I nomi dei file di classe e le directory devono essere conformi ai requisiti della JVM.

Ora potete eseguire lo script e vedere l’attore in azione.

scala -cp . shapes-actor-script.scala

Dovreste vedere l’uscita seguente.

Circle.draw: Circle(Point(0.0,0.0),1.0)
Rectangle.draw: Rectangle(Point(0.0,0.0),2.0,5.0)
Triangle.draw: Triangle(Point(0.0,0.0),Point(1.0,0.0),Point(0.0,1.0))
Errore: messaggio sconosciuto! 3.14159
termino...

Per maggiori informazioni sugli attori, si veda il capitolo 9.

Riepilogo, e poi?

Abbiamo sostenuto la causa di Scala e vi abbiamo mostrato due primi esempi di programmi in Scala, uno dei quali vi ha permesso di farvi un’idea della libreria di attori usata da Scala per la concorrenza. Nel prossimo capitolo approfondiremo la sintassi di Scala, evidenziando diversi modi per sbrigare molto lavoro risparmiando caratteri.


  1. [NdT] Nel corso del 2009, tuttavia, i tre sviluppatori di JRuby e lo sviluppatore principale di Jython che Sun aveva assunto hanno lasciato la compagnia.

© 2008–9 O’Reilly Media
© 2009–10 Giulio Piancastelli per la traduzione italiana