Voi siete qui: Inizio Programmare in Scala

Strumenti, librerie e IDE per Scala

Nell’ultimo capitolo abbiamo visto come progettare applicazioni scalabili in Scala. In questo capitolo illustreremo gli strumenti e le librerie essenziali per il lavoro di chi sviluppa applicazioni in Scala.

Vi abbiamo presentato brevemente gli strumenti di Scala a riga di comando nel capitolo 1. Ora esploreremo questi strumenti nel dettaglio e faremo la conoscenza di altri strumenti essenziali per lo sviluppatore Scala, tra cui alcuni plug-in dedicati al linguaggio per diversi editor e IDE, strumenti di collaudo e varie altre librerie e framework. Non tratteremo questi argomenti in maniera esaustiva, ma vi indicheremo le fonti da consultare per ottenere maggiori informazioni.

Strumenti a riga di comando

Anche se svolgete la maggior parte del vostro lavoro in un IDE, la comprensione del funzionamento degli strumenti a riga di comando vi rende più versatili e vi permette di ripiegare su di essi nel caso in cui gli strumenti grafici dovessero abbandonarvi. In questo capitolo, vi daremo alcuni consigli pratici per interagire con gli strumenti a riga di comando, anche se non ne descriveremo tutte le opzioni. Per approfondire nel dettaglio la conoscenza di questi strumenti, vi raccomandiamo di scaricare e consultare il pacchetto di documentazione chiamato scala-devel-docs, come descritto nella sezione Per maggiori informazioni del capitolo 1 e nella sezione Lo strumento sbaz a riga di comando più avanti in questo capitolo.

Tutti gli strumenti a riga di comando vengono installati nella directory scala-home/bin (si veda la sezione Installare Scala nel capitolo 1).

Lo strumento scalac a riga di comando

Il comando scalac compila i file sorgente Scala e genera file di classe per la JVM. A differenza di quanto accade per Java, il nome del file sorgente non deve obbligatoriamente corrispondere al nome della classe pubblica contenuta nel file. In effetti, in un file potete definire tutte le classi pubbliche che volete, e potete anche usare dichiarazioni di package arbitrarie senza dover collocare i file nelle directory corrispondenti.

Tuttavia, per rispettare i requisiti della JVM, scalac genererà un file di classe separato per ogni tipo, dando al file un nome che corrisponde al nome del tipo (a volte codificato, come nel caso delle definizioni di tipo annidate). Inoltre, i file di classe verranno collocati nelle directory che corrispondono alle dichiarazioni di package. Vedremo un esempio dei tipi di file di classe generati nella prossima sezione, quando esamineremo il comando scala.

Il comando scalac è solo uno script di shell che racchiude un’invocazione del comando java a cui viene passato il nome dell’oggetto Main del compilatore Scala. Lo script aggiunge i file JAR di Scala al CLASSPATH e definisce diverse proprietà di sistema relative a Scala. Il comando si invoca in questo modo.

scalac [opzioni …] [file-sorgente]

Per esempio, abbiamo usato la seguente invocazione del comando scalac nella sezione Un assaggio di Scala del capitolo 1, dove abbiamo creato un semplice strumento a riga di comando per convertire in maiuscolo le stringhe in ingresso.

scalac upper3.scala

La tabella 14.1 mostra la lista delle opzioni per il comando scalac, come riportate da scalac -help.

Tabella 14.1. Le opzioni del comando scalac.

OpzioneDescrizione

-X

Stampa una sinossi delle opzioni avanzate.

-bootclasspath percorso

Ridefinisce l’ubicazione dei file di classe da caricare all’avvio.

-classpath percorso

Specifica dove trovare i file di classe dell’utente.

-d directory

Specifica dove collocare i file di classe generati.

-dependencyfile file

Specifica il file in cui sono indicate le dipendenze (versione 2.8).

-deprecation

Mostra in uscita i punti in cui il codice sorgente fa uso di API deprecate.

-encoding codifica

Specifica la codifica di carattere usata dai file sorgente.

-explaintypes

Spiega gli errori di tipo in maniera più dettagliata.

-extdirs directory

Ridefinisce l’ubicazione delle estensioni del compilatore installate.

-g:livello

Specifica il livello delle informazioni di debug generate: none, source, line, vars, notailcalls.

-help

Stampa una sinossi delle opzioni standard.

-make:strategia

Specifica la strategia usata per individuare i file da ricompilare (versione 2.8): all, changed, immediate, transitive.

-nowarn

Evita di generare messaggi di warning.

-optimise

Genera bytecode più veloce ottimizzando il programma.

-print

Stampa il programma rimuovendo tutte le caratteristiche specifiche di Scala.

-sourcepath percorso

Specifica dove trovare i file sorgente da compilare.

-target:obiettivo

Specifica per quale JVM devono essere generati i file di classe: jvm-1.5, jvm-1.4, msil.

-unchecked

Stampa messaggi di warning dettagliati che indicano i possibili problemi dovuti alla cancellazione di tipo.

-uniqid

Stampa gli identificatori con un nome unico (utile in fase di debug).

-verbose

Mostra messaggi che descrivono quali operazioni sta eseguendo il compilatore.

-version

Stampa la versione del prodotto ed esce.

@ file

Un file di testo contenente gli argomenti per il compilatore (opzioni e file sorgente).

Raccomandiamo di usare abitualmente le opzioni -deprecation e -unchecked: aiutano a prevenire alcuni bug e vi incoraggiano a eliminare l’uso di librerie obsolete.

Le opzioni avanzate attivate da -X controllano la verbosità dei messaggi, regolano il comportamento del compilatore, compreso l’uso di plug-in ed estensioni sperimentali, &c. Esamineremo l’opzione -Xscript quando parleremo del comando scala nella prossima sezione.

Le opzioni avanzate -Xfuture e -Xcheckinit sono utili per risolvere il problema della ridefinizione dei valori val descritto nella sezione Ridefinire i campi astratti e concreti nei tratti che affligge le versioni 2.7.X di Scala. Similmente, l’opzione -Xexperimental abilita modifiche sperimentali e genera messaggi di warning per indicare le modifiche di comportamento potenzialmente rischiose. Si veda la sezione Ridefinire i campi astratti e concreti nei tratti per i dettagli.

Una caratteristica importante di scalac è la sua architettura a plug-in, che è stata significativamente migliorata nella versione 2.8. I plug-in del compilatore possono essere introdotti in ogni fase della compilazione, abilitando trasformazioni di codice, analisi, &c. Per esempio, Scala 2.8 includerà un plug-in per le continuazioni che gli sviluppatori potranno usare per fare in modo che il bytecode generato sfrutti un modello di esecuzione basato sul passaggio di continuazioni (CPS) anziché un modello basato su stack. Tra gli altri plug-in che sono in fase di sviluppo segnaliamo un analizzatore di “effetti”, utile per determinare se le funzioni sono davvero prive di effetti collaterali, se le variabili vengono modificate, &c. Infine, una versione preliminare dello strumento di documentazione sxr [SXR] usa un plug-in del compilatore per generare la documentazione del codice Scala sotto forma di ipertesto.

Per avere maggiori informazioni su scalac potete leggere la documentazione degli strumenti per gli sviluppatori, installabile tramite il comando sbaz illustrato più avanti nella sezione Lo strumento sbaz a riga di comando. In particolare, la tabella 14.4 mostra un’invocazione di esempio del comando sbaz che installa la documentazione scala-devel-docs.

Il bytecode generato da Scala 2.8 non sarà pienamente compatibile con il bytecode generato dalla versione 2.7.5. La compatibilità a livello di codice sorgente verrà comunque preservata nella maggior parte dei casi. Se avete realizzato una vostra implementazione delle collezioni, essa potrebbe richiedere alcune modifiche.

Lo strumento scala a riga di comando

Anche il comando scala è uno script di shell che racchiude un’invocazione al comando java. Lo script aggiunge i file JAR di Scala al CLASSPATH e definisce diverse proprietà di sistema relative a Scala. Il comando si invoca in questo modo.

scala [opzioni …] [script-o-oggetto] [argomenti]

Per esempio, dopo aver compilato il nostro file upper3.scala proveniente dalla sezione Un assaggio di Scala del capitolo 1, come abbiamo appena fatto durante la precedente disamina di scalac, possiamo eseguire l’“applicazione” in questo modo.

scala -cp . Upper Ciao Mondo!

L’opzione -cp . aggiunge la directory di lavoro corrente al percorso di ricerca delle classi. Upper è il nome della classe che contiene il metodo main da eseguire. Ciao Mondo! sono gli argomenti passati a Upper. Questo comando produce l’uscita seguente.

CIAO MONDO!

Il comando decide cosa fare sulla base dello script-o-oggetto specificato. Se non specificate uno script o un oggetto, scala viene eseguito come un interprete interattivo in cui digitare codice che viene valutato sul momento, un ambiente talvolta chiamato REPL (Read-Evaluate-Print Loop, letteralmente ciclo di lettura-valutazione-stampa). La modalità interattiva vi mette a disposizione alcuni comandi speciali; digitate :help per vederne l’elenco.

La versione 2.8 aggiunge molti miglioramenti al REPL, incluso il completamento automatico del codice.

Il nostro esempio di Upper illustra il caso in cui specificate il nome completamente qualificato di un object (o il nome di una classe Java). In questo caso, scala si comporta proprio come il comando java: cerca il codice corrispondente nel CLASSPATH e si aspetta di trovare un metodo main nel tipo. Ricordatevi che, per i tipi Scala, dovete definire i metodi main negli object. Gli argomenti vengono passati come argomenti al metodo main.

Se specificate un file sorgente Scala come script-o-oggetto, scala interpreta il file come uno script (cioè lo compila e lo esegue). Molti esempi nel libro vengono invocati in questo modo. Gli argomenti vengono resi disponibili allo script nell’array args. Ecco uno script di esempio che implementa la stessa funzione di trasformazione in maiuscolo.

// esempi/cap-14/upper-script.scala

args.map(_.toUpperCase()).foreach(printf("%s ",_))
println("")

Se eseguiamo questo script con il comando scala upper.scala Ciao Mondo!, otteniamo la stessa uscita di prima, CIAO MONDO!.

Infine, se invocate scala senza un file di script o il nome di un oggetto come argomento, scala viene eseguito in modalità interattiva. Ecco un esempio di una sessione interattiva.

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

scala> "Programmare in Scala" foreach { c => println(c) }
P
r
o
g
…

Il comando scalac accetta tutte le opzioni accettate da scalac (si veda la tabella 14.1) più le opzioni elencate nella tabella 14.2.

Tabella 14.2. Le opzioni del comando scala (in aggiunta alle opzioni del comando scalac).

OpzioneDescrizione

-howtorun script

Interpreta esplicitamente script-o-oggetto come un file di script.

-howtorun object

Interpreta esplicitamente script-o-oggetto come un oggetto compilato.

-howtorun guess

Determina autonomamente la natura di script-o-oggetto (predefinito).

-i file

Precarica file. Questa opzione è significativa solo per le shell interattive.

-e argomento

Interpreta argomento come codice Scala.

-savecompiled

Salva lo script compilato per usarlo in futuro.

-nocompdaemon

Evita di usare fsc, il compilatore offline. (Si veda la sezione Lo strumento fsc a riga di comando.)

-Dproprietà=valore

Imposta una proprietà di sistema Java a valore.

Usate l’opzione -i file nella modalità interattiva quando volete precaricare un file prima di digitare comandi. Una volta nella shell, potete anche caricare un file usando il comando :load nomefile. La tabella 14.3 elenca gli speciali comandi della forma :comando disponibili nell’ambito della modalità interattiva di scala.

Tabella 14.3. Comandi disponibili nell’ambito della modalità interattiva di scala.

OpzioneDescrizione

:help

Stampa un messaggio di aiuto riguardante questi comandi.

:load

Seguito dal nome di un file, carica un file Scala.

:replay

Riporta il sistema allo stato iniziale e riesegue tutti i comandi precedenti.

:quit

Esce dall’interprete.

:power

Abilita la modalità utente potenziata di Scala 2.8.

La nuova “modalità utente potenziata” aggiunge nuovi comandi per esaminare i dati in memoria, come l’albero sintattico astratto e le proprietà dell’interprete, e per eseguire ulteriori operazioni.

Per l’esecuzione in modalità batch usate l’opzione -e argomento per specificare il codice Scala da interpretare. Se state usando una shell che supporta la redirezione I/O (per esempio la shell Bourne, la shell C, o i loro discendenti) e avete bisogno di costruire dinamicamente righe di codice, potete anche passare il codice all’interprete scala usando una pipe, come mostrato nello script bash poco significativo che segue.

#!/usr/bin/env bash
# esempi/cap-14/pipe-example.sh

c=Ciao
m=Mondo
function commands {
cat <<-EOF
println("$c")
println("$m")
EOF
}

commands | scala

Invocare gli script con scala è fastidioso quando li usate frequentemente. Su Windows e sui sistemi UNIX potete creare script Scala che non richiedono l’uso dell’invocazione scala nome-file-script.

Per i sistemi UNIX, l’esempio seguente mostra come realizzare uno script eseguibile. Non dimenticatevi di rendere il file eseguibile, per esempio tramite il comando chmod +x secho.

#!/bin/sh
exec scala "$0" "$@"
!#
print("Hai digitato: ")
argv.toList foreach { s => format("%s ", s) }
println

Ecco come potreste usarlo.

$ secho Ciao Mondo
Hai digitato: Ciao Mondo

Similmente, ecco un esempio di comando .bat in Windows.

::#!
@echo off
call scala %0 %*
goto :eof
::!#
print("Hai digitato: ")
argv.toList foreach { s => format("%s ", s) }
println

Si veda la pagina di manuale relativa a scala nel pacchetto di documentazione per lo sviluppatore scala-devel-docs per trovare maggiori informazioni su tutte le opzioni del comando scala.

Limitazioni di scala rispetto a scalac

L’esecuzione di un file sorgente con scala è soggetta ad alcune limitazioni rispetto alla compilazione con scalac.

Qualsiasi script eseguito con scala viene racchiuso in un object anonimo che somiglia più o meno all’esempio seguente.

// esempi/cap-14/script-wrapper.scala

object Script {
  def main(args: Array[String]): Unit = {
    new AnyRef {
      // Il codice del vostro script viene inserito qui.
    }
  }
}

Al momento della scrittura, gli object Scala non possono racchiudere dichiarazioni di package, e quindi non potete dichiarare package negli script. Questo è il motivo per cui gli esempi di questo libro che dichiarano package devono essere compilati ed eseguiti separatamente, come accade per questo esempio proveniente dal capitolo 2.

// esempi/cap-2/package-example1.scala

package com.example.mypkg

class MyClass {
  // ...
}

Specularmente, ci sono script validi che non possono essere compilati con scalac a meno di non usare una speciale opzione -X. Per esempio, le definizioni di funzione e le invocazioni di funzione al di fuori dei tipi non sono permesse. L’esempio seguente viene eseguito senza problemi da scala.

// esempi/cap-14/example-script.scala

case class Message(name: String)

def printMessage(msg: Message) = {
  println(msg)
}

printMessage(new Message(
    "Bisogna compilare questo script con scalac -Xscript <nome>!"))

Come ci aspettiamo, l’esecuzione di questo script con scala produce l’uscita seguente.

Message(Bisogna compilare questo script con scalac -Xscript <nome>!)

Tuttavia, se provate a compilare lo script con scalac (senza l’opzione -Xscript), ottenete il seguente errore.

example-script.scala:3: error: expected class or object definition
def printMessage(msg: Message) = {
^
example-script.scala:7: error: expected class or object definition
printMessage(new Message("Bisogna compilare questo script con scalac -Xscript <nome>!"))
^
two errors found

È lo stesso script a descrivere la soluzione: per compilarlo con scalac dovete aggiungere l’opzione -Xscript nome, dove nome è il nome che volete dare al file di classe compilato. Per esempio, usando MessagePrinter come nome verranno creati diversi file di classe il cui nome contiene il prefisso MessagePrinter.

scalac -Xscript MessagePrinter example-script.scala

Ora potete eseguire il codice compilato con il comando:

scala -classpath . MessagePrinter

La directory corrente conterrà i seguenti file di classe.

MessagePrinter$$anon$1$Message$.class
MessagePrinter$$anon$1$Message.class
MessagePrinter$$anon$1.class
MessagePrinter$.class
MessagePrinter.class

Cosa sono tutti questi file? MessagePrinter e MessagePrinter$ vengono generati da scalac per racchiudere il punto di ingresso dello script come “applicazione”. Ricordatevi che abbiamo indicato MessagePrinter come nome per l’argomento di -Xscript, perciò MessagePrinter contiene il metodo static main che ci serve.

MessagePrinter$$anon$1 è una classe generata che racchiude l’intero script. Il metodo printMessage definito nello script è un metodo di questa classe. La classe Message e il suo oggetto associato dichiarati nello script vengono annidati dentro la classe MessagePrinter$$anon$1 generata per l’intero script e corrispondono rispettivamente alle classi MessagePrinter$$anon$1$Message e MessagePrinter$$anon$1$Message$. Se volete vedere il contenuto di questi file di classe, usate uno dei decompilatori descritti nella prossima sezione.

Gli strumenti scalap, javap e jad a riga di comando

Esistono diversi decompilatori che possono aiutarvi a capire come i costrutti del linguaggio Scala vengono ricondotti alla macchina virtuale sottostante. I decompilatori sono particolarmente utili quando avete bisogno di invocare codice Scala da Java e volete sapere come i nomi Scala vengono trasformati in nomi compatibili con la JVM o volete capire come fa il compilatore Scala a tradurre le caratteristiche del linguaggio in bytecode valido.

Esamineremo tre decompilatori e i vantaggi offerti da ognuno. Dato che i file di classe generati da scalac contengono bytecode valido per la JVM, è possibile usare gli strumenti di decompilazione offerti da Java.

MessagePrinter.class è uno dei file di classe generati dallo script di esempio della sezione precedente. Se lo usiamo per eseguire scalap -classpath . MessagePrinter, otteniamo l’uscita seguente.

package MessagePrinter;
final class MessagePrinter extends scala.AnyRef {
}
object MessagePrinter {
  def main(scala.Array[java.lang.String]): scala.Unit;
  def $tag(): scala.Int;
    throws java.rmi.RemoteException
}

Notate che il primo metodo all’interno di object MessagePrinter è il metodo main. Il metodo $tag è parte dell’implementazione interna di Scala: è un metodo astratto, definito da ScalaObject, di cui il compilatore genera automaticamente implementazioni per i tipi concreti. Il metodo $tag è stato originariamente introdotto per ottimizzare il pattern matching, ma ora è deprecato e potrebbe essere rimosso in una prossima versione di Scala.

Confrontiamo l’uscita di scalap con il risultato della esecuzione di javap -classpath . MessagePrinter.

Compiled from "(virtual file)"
public final class MessagePrinter extends java.lang.Object{
  public static final void main(java.lang.String[]);
  public static final int $tag()       throws java.rmi.RemoteException;
}

Ora vediamo la dichiarazione di main così come la vedremmo in un tipico file sorgente Java.

Infine, potete usare jad passandogli semplicemente il nome del file di classe, e lo strumento genererà un file di uscita corrispondente con l’estensione .jad. Se eseguite jad MessagePrinter.class, otterrete un lungo file chiamato MessagePrinter.jad, e anche diversi messaggi di warning che vi informano di come jad non abbia potuto decompilare completamente alcuni metodi. Eviteremo di riprodurre il risultato del comando in questa sede; sappiate comunque che il file .jad conterrà normali istruzioni Java alternate a diverse sezioni di istruzioni in bytecode nei punti in cui lo strumento non è riuscito a effettuare la decompilazione.

Tutti questi strumenti sono dotati di una guida a riga di comando.

La documentazione per lo sviluppatore Scala contiene il manuale di scalap. Un documento simile per javap è incluso nel JDK. La distribuzione di jad è accompagnata da un file README con la documentazione. Le distribuzioni per Mac OS X e Linux includono anche una pagina man.

Infine, come esercizio, provate a compilare la classe Complex molto semplice riprodotta di seguito, usata per rappresentare i numeri complessi. Poi eseguite scalap, javap e jad sui file di classe risultanti.

// esempi/cap-14/complex.scala

case class Complex(real: Double, imaginary: Double) {
  def +(that: Complex) =
    new Complex(real + that.real, imaginary + that.imaginary)
  def -(that: Complex) =
    new Complex(real - that.real, imaginary - that.imaginary)
}

Come vengono codificati i metodi + e -? Quali sono i nomi dei metodi di lettura per i campi real e imaginary? Quali tipi Java vengono usati per i campi?

Lo strumento scaladoc a riga di comando

Il comando scaladoc è analogo a javadoc: viene usato per generare documentazione a partire dai file sorgente Scala; questa documentazione viene chiamata Scaladoc. Il riconoscitore di scaladoc supporta le stesse annotazioni (indicate dal simbolo @) di javadoc, come @author, @param, &c.

Se usate scaladoc per la vostra documentazione, potreste voler considerare l’impiego di vscaladoc, uno strumento scaladoc migliorato che è disponibile all’indirizzo http://code.google.com/p/vscaladoc/. La documentazione di vscaladoc si può anche trovare su [ScalaTools].

Lo strumento sbaz a riga di comando

Lo Scala Bazaar System (sbaz) è un sistema di impacchettamento che vi assiste nella manutenzione automatica di un’installazione di Scala. È analogo al sistema di impacchettamento gem di Ruby, al CPAN di Perl, &c.

Il sito di Scala ospita una descrizione breve ma efficace di come usare sbaz all’indirizzo http://www.scala-lang.org/node/93. Tutte le opzioni a riga di comando sono descritte nella documentazione per lo sviluppatore. La tabella seguente riassume le opzioni più utili.

Tabella 14.4. Le opzioni più utili per il comando sbaz.

ComandoDescrizione

sbaz showuniverse

Mostra l’“universo” corrente (il repository remoto). L’ubicazione predefinita è http://scala-webapps.epfl.ch/sbaz/scala-dev.

sbaz setuniverse univ

Punta a un nuovo “universo” univ.

sbaz installed

Quali pacchetti sono già installati localmente?

sbaz available

Quali meraviglie sono disponibili su Internet?

sbaz install scala-devel-docs

Installa l’indispensabile pacchetto scala-devel-docs (per esempio).

sbaz upgrade

Aggiorna tutti i pacchetti installati all’ultima versione.

Notate che un repository remoto usato da sbaz viene chiamato “universo”.

Lo strumento fsc a riga di comando

Il compilatore Scala veloce (in inglese, fast scala compiler, dalle cui iniziali deriva il nome dello strumento) viene messo in esecuzione come processo demone in modo che il compilatore possa essere invocato più velocemente, eliminando in particolare il costo aggiuntivo di avvio. Questo strumento è particolarmente utile per le esecuzioni ripetute di uno script (per esempio, quando rieseguite una serie di test fino a riprodurre un bug). In effetti, fsc viene invocato automaticamente dal comando scala, ma potete anche invocarlo direttamente.

Strumenti di assemblaggio

Sono stati implementati plug-in Scala per diversi strumenti di assemblaggio comunemente usati, compresi Ant (http://ant.apache.org/), Maven (http://maven.apache.org/) e Buildr (http://buildr.apache.org/). Esistono anche diversi strumenti di assemblaggio scritti in Scala e destinati specificamente allo sviluppo con Scala; forse l’esempio più noto di questi strumenti è sbt (“Simple Build Tool” [SBT]). Questi plug-in e strumenti sono documentati molto bene sui rispettivi siti web, quindi vi rimandiamo a quei siti per i dettagli.

La distribuzione Scala include alcune attività di Ant per scalac, fsc e scaladoc, utilizzabili in modo molto simile ai loro corrispondenti Java; esse sono descritte all’indirizzo http://scala-lang.org/node/98.

Il plug-in Scala per Maven è disponibile all’indirizzo http://scala-tools.org/mvnsites/maven-scala-plugin/. Non richiede che Scala sia installato, dato che lo scaricherà per voi. Diversi progetti Scala di terze parti usano Maven, come per esempio Lift (si veda la sezione Lift più avanti in questo capitolo).

Buildr è un progetto Apache disponibile all’indirizzo http://buildr.apache.org/. È destinato ad applicazioni scritte per la JVM in qualsiasi linguaggio, con un supporto predefinito per Scala, Groovy e ovviamente Java. È compatibile con i repository e i layout di progetto di Maven. Dato che i programmi di assemblaggio sono scritti in Ruby, tendono a essere molto più concisi rispetto ai corrispondenti file Maven. Buildr è anche utile per testare le applicazioni scritte per la JVM con gli strumenti di collaudo di Ruby, come RSpec (http://rspec.info) e Cucumber (http://cukes.info), nel caso usiate JRuby (http://jruby.codehaus.org/) per assemblare le vostre applicazioni.

Lo strumento di assemblaggio sbt dedicato alle applicazioni Scala è disponibile all’indirizzo http://code.google.com/p/simple-build-tool/ e presenta alcune somiglianze con Buildr. È anche compatibile con Maven, ma usa Scala come linguaggio per scrivere gli script di assemblaggio. Inoltre, è dotato di un supporto predefinito per generare la documentazione Scaladoc e per effettuare il collaudo con ScalaTest, Specs e ScalaCheck.

Integrazione con gli IDE

Se avete una certa esperienza di programmazione in Java, probabilmente siete stati viziati dalle ricche funzionalità degli attuali IDE per quel linguaggio. Il supporto degli IDE per Scala non è ancora così avanzato, ma si sta evolvendo rapidamente in Eclipse, IntelliJ IDEA e NetBeans. Al momento della scrittura, i plug-in Scala per questi tre IDE supportano tutti la colorazione della sintassi, la gestione dei progetti, un numero limitato di refactoring automatici, &c. Sebbene ogni plug-in abbia particolari vantaggi rispetto agli altri, le loro funzionalità sono abbastanza simili da permettervi di adottare il plug-in per il vostro IDE preferito senza essere costretti a rinunce inaccettabili.

In questa sezione descriveremo come usare il supporto per Scala disponibile in Eclipse, IntelliJ IDEA e NetBeans, supponendo che sappiate già come usare questi IDE per lo sviluppo in altri linguaggi, come Java.

Eclipse

Installare il plug-in per Scala

Per conoscere i dettagli sul plug-in Eclipse per Scala, partite dalla pagina web http://www.scala-lang.org/node/94. Se siete interessati a contribuire allo sviluppo del plug-in, visitate la pagina web http://lampsvn.epfl.ch/trac/scala/wiki/EclipsePlugin.

Il plug-in richiede il JDK versione 5 o superiore (viene raccomandata la 6) ed Eclipse 3.3 o superiore (viene raccomandata la versione 3.4). Il plug-in installa autonomamente lo SDK per Scala. Per installare il plug-in, invocate il comando Software Updates nel menu Help.

Selezionate la scheda Available Software e cliccate sul pulsante “Add Site…” situato a destra. Vedrete la finestra di dialogo mostrata in figura 14.1.

Figura 14.1. La finestra di dialogo Add site di Eclipse.

Digitate l’URL mostrato in figura, http://www.scala-lang.org/scala-eclipse-plugin. Alcune persone preferiscono lavorare con i rilasci notturni che si trovano all’indirizzo http://www.scala-lang.org/scala-eclipse-plugin-nightly, ma sappiate che non viene data alcuna garanzia sul loro funzionamento!

Selezionate la casella di controllo vicino al sito di aggiornamento appena aggiunto e cliccate sul pulsante Install, come indicato nella figura 14.2. Non cliccate sul pulsante “predefinito” Close!

La finestra di dialogo Software Updates and Add-ons.

Fate attenzione: la scarsa usabilità della finestra di dialogo Software Updates potrebbe confondervi facilmente.

Dopo che il plug-in è stato trovato sul sito di aggiornamento, vi verrà presentata una finestra di dialogo Install. Superate a colpi di mouse la sequenza di schermate per completare l’installazione. Vi verrà chiesto di riavviare Eclipse quando l’installazione sarà completata.

Sviluppare applicazioni in Scala

Una volta che il plug-in è stato installato, usando la voce di menu File → New → Other… potrete creare progetti Scala. Troverete una cartella Scala Wizards che contiene una voce chiamata Scala Project dal funzionamento molto simile alla familiare Java Project.

Potete lavorare sul vostro progetto Scala usando la maggior parte degli stessi comandi che usereste con un tipico progetto Java. Per esempio, potete creare un nuovo tratto, una nuova classe o un nuovo oggetto usando il menu contestuale.

Il plug-in Eclipse per Scala presenta ancora alcuni “spigoli”, ma gli sviluppatori Scala che usano Eclipse dovrebbero trovarlo accettabile per le loro necessità quotidiane.

IntelliJ

Installare il plug-in per Scala

I creatori di IntelliJ IDEA distribuiscono un plug-in Scala di qualità beta. Per conoscere i dettagli, partite dalla pagina web http://www.jetbrains.net/confluence/display/SCA/Scala+Plugin+for+IntelliJ+IDEA.

Per usare il plug-in, dovete usare le versioni 8.0.X di IntelliJ o una versione successiva. Considerate la possibilità di usare il rilascio “EAP” più recente per avvalervi degli ultimi aggiornamenti alle funzionalità di IDEA. Dovete anche avere lo SDK Scala a riga di comando già installato, come descritto nella sezione Installare Scala del capitolo 1.

Per installare il plug-in Scala, avviate IDEA. Aprite il pannello Settings, per esempio usando la voce di menu File → Settings. Scorrete l’elenco sul lato sinistro e cliccate sull’elemento Plugins, come mostrato nella figura 14.3.

Figura 14.3. La voce per le impostazioni dei plug-in in IntelliJ IDEA.

Selezionate la scheda Available sul lato destro. Scorrete l’elenco fino al plug-in Scala, come mostrato nella figura 14.4.

Figura 14.4. Il plug-in Scala disponibile per IntelliJ IDEA.

Cliccate con il tasto destro sul nome del plug-in Scala e selezionate Download and Install dal menu. Ripetete l’operazione per il plug-in Scala Application. Dovrete riavviare IDEA per abilitare i plug-in.

Dopo aver riavviato IDEA, verificate che i due plug-in siano stati installati correttamente riaprendo il Plugin Manager. Selezionate la scheda Installed e scorrete l’elenco per trovare i due plug-in per Scala. Dovrebbero essere visualizzati in nero e le caselle di controllo al loro fianco dovrebbero essere spuntate, come mostrato in figura 14.5.

Figura 14.5. I plug-in IntelliJ IDEA per Scala installati.

Se i plug-in vengono mostrati in rosso o le caselle di controllo non sono spuntate, fate riferimento alla pagina web del plug-in Scala già citata per cercare di capire cosa non ha funzionato.

Sviluppare applicazioni in Scala

Per creare un progetto Scala in IDEA cominciate col selezionare la voce di menu File → New Project. Nella finestra di dialogo, selezionate il pulsante radio appropriato al lavoro che dovete fare, per esempio “Create New Project from Scratch”.

Nella schermata successiva, selezionate Java Module e fornite le consuete informazioni di progetto. Un esempio viene mostrato nella figura 14.6.

Figura 14.6. Specificare i dettagli per un progetto Scala in IntelliJ IDEA.

Passate attraverso la schermata intitolata Please Select Desired Technology. Spuntate le caselle di controllo Scala e New Scala SDK. Cliccate sul pulsante etichettato “…” per raggiungere l’ubicazione in cui si trova la vostra installazione dello SDK Scala, come mostrato in figura 14.7. Sarà necessario specificare lo SDK solo la prima volta che create un progetto o quando installate un nuovo SDK in una posizione differente.

Figura 14.7. Aggiungere Scala a un progetto IntelliJ IDEA.

Cliccate su Finish. Vi verrà chiesto se volete creare un Project o una Application. Selezionate Application se volete condividere questo progetto con altri progetti Scala sullo stesso computer.

Ora potete lavorare sul vostro progetto Scala usando la maggior parte degli stessi comandi che usereste con un tipico progetto Java. Per esempio, potete creare un nuovo tratto, una nuova classe o un nuovo oggetto usando il menu contestuale, come per i progetti Java.

Il plug-in IntelliJ IDEA per Scala è ancora in versione beta, ma gli sviluppatori Scala che usano IDEA dovrebbero trovarlo accettabile per le loro esigenze quotidiane.

NetBeans

Installare il plug-in per Scala

NetBeans è dotato di un plug-in per Scala di qualità beta. Per conoscere i dettagli, partite dalla pagina web http://wiki.netbeans.org/Scala. Viene richiesta la versione 6.5 di NetBeans, o un rilascio notturno più recente. Il plug-in Scala contiene una versione dello SDK Scala. La pagina wiki fornisce istruzioni per usare un SDK differente, se lo desiderate.

Per installare il plug-in scaricate l’archivio compresso che lo contiene da http://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544. Estraetene i contenuti in una directory di vostra scelta.

Avviate NetBeans e invocate la voce di menu Tools → Plugins. Selezionate la scheda Downloaded e cliccate sul pulsante Add Plugins…; poi scegliete la directory dove avete estratto il plug-in Scala e selezionate tutti i file .nbm elencati, come mostrato nella figura 14.8. Infine cliccate sul pulsante Open.

Figura 14.8. Aggiungere il plug-in Scala per l’installazione.

Tornati alla finestra di dialogo Plugins, assicuratevi che tutte le caselle di controllo relative ai nuovi plug-in siano spuntate. Cliccate sul pulsante Install.

Passate attraverso la finestra di dialogo per l’installazione e riavviate NetBeans quando il processo è terminato.

Sviluppare applicazioni in Scala

Per creare un progetto Scala in NetBeans cominciate col selezionare la voce di menu File → New Project o cliccate sul pulsante New Project. Nella finestra di dialogo, selezionate Scala sotto Categories e Scala Application sotto Projects, come mostrato in figura 14.9. Cliccate su Next.

Figura 14.9. Creare un progetto Scala in NetBeans.

Fornite il nome del progetto, l’ubicazione, &c. e cliccate su Finish.

Una volta che il progetto è stato creato, potete lavorare su di esso usando la maggior parte degli stessi comandi che usereste con un tipico progetto Java. Ci sono alcune differenze. Per esempio, quando invocate l’elemento New nel menu contestuale, il sottomenu non mostra le voci per creare nuovi tipi Scala; invece, dovete invocare la voce di menu Other… e passare attraverso una finestra di dialogo. Questa discrepanza verrà risolta in una futura versione.

Nonostante alcuni problemi di minore importanza come questo, il plug-in NetBeans per Scala è abbastanza maturo per essere usato regolarmente.

Editor di testo

Lo strumento sbaz gestisce il pacchetto scala-tool-support che include plug-in Scala per diversi editor, compresi Emacs, Vim, TextMate e altri. Come sbaz, anche il pacchetto scala-tool-support viene distribuito insieme a Scala. Per sapere quali sono gli editor supportati, potete esaminare le directory presenti in scala-home/misc/scala-tool-support. La maggior parte delle directory specifiche per un editor contiene istruzioni per installare il plug-in; negli altri casi, consultate le istruzioni del vostro editor per installare plug-in di terze parti.

Alcuni pacchetti sono piuttosto immaturi. Se volete offrire il vostro contributo alla comunità Scala, vi preghiamo di considerare la possibilità di migliorare la qualità dei plug-in esistenti o di sviluppare nuovi plug-in.

Al momento della scrittura, esistono diverse variazioni di un “bundle” Scala per l’editor TextMate, che è un popolare editor di testi per Mac OS X. Attualmente, questi bundle sono gestiti da Paul Phillips su GitHub all’indirizzo http://github.com/paulp/scala-textmate.1 È lecito sperare che le migliori caratteristiche di ogni bundle verranno unificate in un bundle “autoritativo” e integrate nel pacchetto scala-tool-support.

Sviluppo guidato dai test in Scala

Una delle pratiche di sviluppo più importanti introdotte nell’ultima decade è lo sviluppo guidato dai test (o TDD, dall’inglese test-driven development). La comunità Scala ha creato diversi strumenti per supportare il TDD.

Se lavorate in un’azienda dove si programma esclusivamente in Java, considerate la possibilità di adottare uno o più di questi strumenti di collaudo scritti in Scala per guidare lo sviluppo del vostro codice Java tramite i test. Questa è una strategia a basso rischio per introdurre Scala nel vostro ambiente, in modo da acquisire una certa esperienza con il linguaggio prima di assumere l’impegno di usarlo nel codice di produzione. In particolare, potreste fare esperimenti con ScalaTest (si veda la prossima sezione, intitolata ScalaTest), che può essere usato con JUnit [JUnit] e TestNG [TestNG], oppure potreste considerare ScalaCheck o Reductio (si veda la sezione ScalaCheck più avanti), le cui caratteristiche innovative potrebbero non essere presenti nei framework di collaudo di Java. Tutti gli strumenti che descriveremo si integrano con gli strumenti di collaudo e di assemblaggio di Java, come JUnit, TestNG, varie librerie di mock objects (oggetti “finti”), Ant, Maven [Maven], e tutti vi permettono di scrivere i test in Scala sfruttando ognuno il proprio DSL.

ScalaTest

La variante Scala della venerabile libreria XUnit si chiama ScalaTest ed è scaricabile dal sito http://www.artima.com/scalatest/.

Potete eseguire i vostri test utilizzando la classe Runner predefinita oppure sfruttare l’integrazione offerta con JUnit o TestNG. ScalaTest include anche un’attività per Ant e si integra con lo strumento di collaudo ScalaCheck descritto più avanti.

Oltre a supportare l’uso delle asserzioni e dei metodi di test nel tradizionale stile XUnit, ScalaTest vi offre una sintassi per scrivere i test secondo le regole dello sviluppo guidato dal comportamento [BDD], una pratica che sta diventando sempre più popolare. Il sito web di ScalaTest presenta alcuni esempi per queste e altre possibilità.

Ecco un esempio di un test ScalaTest per la semplice classe Complex usata in precedenza nella sezione Gli strumenti scalap, javap e jad a riga di comando.

// esempi/cap-14/complex-test.scala

import org.scalatest.FunSuite

class ComplexSuite extends FunSuite {

  val c1 = Complex(1.2, 3.4)
  val c2 = Complex(5.6, 7.8)

  test("addizione con (0, 0)") {
    assert(c1 + Complex(0.0, 0.0) === c1)
  }

  test("sottrazione con (0, 0)") {
    assert(c1 - Complex(0.0, 0.0) === c1)
  }

  test("addizione") {
    assert((c1 + c2).real === (c1.real + c2.real))
    assert((c1 + c2).imaginary === (c1.imaginary + c2.imaginary))
  }

  test("sottrazione") {
    assert((c1 - c2).real === (c1.real - c2.real))
    assert((c1 - c2).imaginary === (c1.imaginary - c2.imaginary))
  }
}

Questo particolare esempio usa per ogni test la sintassi a “valori funzione” che viene supportata dal tratto genitore FunSuite. Ogni invocazione a test riceve come argomenti una stringa di descrizione e un letterale funzione con l’effettivo codice di collaudo.

Il comando seguente compila complex.scala e complex-test.scala, collocando i file di classe nella directory build, e poi esegue i test. Supporremo che scalatest-0.9.5.jar (l’ultima versione al momento della scrittura) si trovi nella directory ../lib, in quanto la distribuzione scaricabile degli esempi di codice è organizzata in questo modo.

scalac -classpath ../lib/scalatest-0.9.5.jar -d build complex.scala complex-test.scala
scala -classpath build:../lib/scalatest-0.9.5.jar org.scalatest.tools.Runner -p build -o -s ComplexSuite

L’uscita prodotta è la seguente.

Run starting. Expected test count is: 4
Suite Starting - ComplexSuite: The execute method of a nested suite is about to be invoked.
Test Starting - ComplexSuite: addizione con (0, 0)
Test Succeeded - ComplexSuite: addizione con (0, 0)
Test Starting - ComplexSuite: sottrazione con (0, 0)
Test Succeeded - ComplexSuite: sottrazione con (0, 0)
Test Starting - ComplexSuite: addizione
Test Succeeded - ComplexSuite: addizione
Test Starting - ComplexSuite: sottrazione
Test Succeeded - ComplexSuite: sottrazione
Suite Completed - ComplexSuite: The execute method of a nested suite returned normally.
Run completed. Total number of tests run was: 4
All tests passed.

Specs

Ispirata alla libreria RSpec [RSpec] di Ruby, la libreria Specs [ScalaSpecsTool] è uno strumento di collaudo per Scala orientato allo sviluppo guidato dal comportamento [BDD]. In breve, il BDD si prefigge lo scopo di rielaborare la sintassi tradizionale dei test in una forma che enfatizzi meglio il ruolo del TDD come processo guida per le attività di progettazione, durante le quali bisognerebbe implementare la “specifica” dei requisiti. La sintassi degli strumenti tradizionali per il TDD come i framework XUnit tende a enfatizzare il ruolo di collaudo del TDD. Riallineando la sintassi, si crede sia più probabile che lo sviluppatore si concentri sul ruolo principale del TDD: guidare la progettazione dell’applicazione.

Potete trovare la documentazione di Specs su [ScalaTools].

Abbiamo già usato Specs in numerosi esempi nel corso del libro; ricordiamo ButtonObserverSpec nella sezione I tratti come mixin del capitolo 4, per citarne uno. Ecco un altro esempio di specifica per la semplice classe Complex già mostrata in precedenza.

// esempi/cap-14/complex-spec.scala

import org.specs._

object ComplexSpec extends Specification {
  "L'addizione tra un numero complesso e (0.0, 0.0)" should {
    "restituire un numero N' che è identico al numero originale N" in {
      val c1 = Complex(1.2, 3.4)
      (c1 + Complex(0.0, 0.0)) mustEqual c1
    }
  }
  "La sottrazione tra un numero complesso e (0.0, 0.0)" should {
    "restituire un numero N' che è identico al numero originale N" in {
      val c1 = Complex(1.2, 3.4)
      (c1 - Complex(0.0, 0.0)) mustEqual c1
    }
  }
  "L'addizione tra numeri complessi" should {
    """restituire un nuovo numero in cui
    le parti reale e immaginaria sono la somma delle parti
    reale e immaginaria dei valori in ingresso, rispettivamente.""" in {
      val c1 = Complex(1.2, 3.4)
      val c2 = Complex(5.6, 7.8)
      (c1 + c2).real mustEqual (c1.real + c2.real)
      (c1 + c2).imaginary mustEqual (c1.imaginary + c2.imaginary)
    }
  }
  "La sottrazione tra numeri complessi" should {
    """restituire un nuovo numero in cui
    le parti reale e immaginaria sono la differenza delle parti
    reale e immaginaria dei valori in ingresso, rispettivamente.""" in {
      val c1 = Complex(1.2, 3.4)
      val c2 = Complex(5.6, 7.8)
      (c1 - c2).real mustEqual (c1.real - c2.real)
      (c1 - c2).imaginary mustEqual (c1.imaginary - c2.imaginary)
    }
  }
}

Un object che estende Specification è l’analogo di una serie di test. Il livello successivo di raggruppamento, per esempio la clausola "L'addizione tra un numero complesso e (0.0, 0.0)" should {…}, incapsula le informazioni al livello del tipo soggetto al collaudo, spesso nella forma di un “insieme” di comportamenti legati tra loro.

La clausola al livello successivo, per esempio "restituire un numero N' che è identico al numero originale N" in {…}, viene chiamata esempio nella terminologia BDD ed è analoga a un singolo test. Come nei tipici framework XUnit, il collaudo viene effettuato usando “esempi rappresentativi” piuttosto che tramite l’esplorazione esaustiva dell’intero “spazio” degli stati possibili, da cui il termine “esempio”. (Tuttavia, si veda la discussione di ScalaCheck più avanti.)

Istruzioni come (c1 + Complex(0.0, 0.0)) mustEqual c1 vengono chiamate aspettative e verificano che le condizioni siano effettivamente soddisfatte. Quindi, le aspettative sono analoghe alle asserzioni negli strumenti XUnit.

Esistono diversi modi per eseguire le vostre specifiche. Dopo aver compilato il file complex-spec.scala appena mostrato, possiamo eseguire la specifica nel modo seguente.

scala -classpath ../lib/specs-1.4.3.jar:build ComplexSpec

Qui, come prima, supponiamo che il file JAR di Specs si trovi nella directory ../lib e che i file di classe compilati siano nella directory build. Otteniamo la seguente uscita.

Specification "ComplexSpec"
  L'addizione tra un numero complesso e (0.0, 0.0) should
  + restituire un numero N' che è identico al numero originale N

  Total for SUT "L'addizione tra un numero complesso e (0.0, 0.0)":
  Finished in 0 second, 0 ms
  1 example, 1 expectation, 0 failure, 0 error

  La sottrazione tra un numero complesso e (0.0, 0.0) should
  + restituire un numero N' che è identico al numero originale N

  Total for SUT "La sottrazione tra un numero complesso e (0.0, 0.0)":
  Finished in 0 second, 0 ms
  1 example, 1 expectation, 0 failure, 0 error

  L'addizione tra numeri complessi should
  + restituire un nuovo numero in cui
        le parti reale e immaginaria sono la somma delle parti
        reale e immaginaria dei valori in ingresso, rispettivamente.

  Total for SUT "L'addizione tra numeri complessi":
  Finished in 0 second, 0 ms
  1 example, 2 expectations, 0 failure, 0 error

  La sottrazione tra numeri complessi should
  + restituire un nuovo numero in cui
        le parti reale e immaginaria sono la differenza delle parti
        reale e immaginaria dei valori in ingresso, rispettivamente.

  Total for SUT "La sottrazione tra numeri complessi":
  Finished in 0 second, 0 ms
  1 example, 2 expectations, 0 failure, 0 error

Total for specification "ComplexSpec":
Finished in 0 second, 37 ms
4 examples, 6 expectations, 0 failure, 0 error

Notate che le stringhe nelle clausole sono scritte in una forma che si può leggere come una specifica dei requisiti.2

…
L'addizione tra un numero complesso e (0.0, 0.0) should
+ restituire un numero N' che è identico al numero originale N
…

Le specifiche si possono eseguire anche tramite un’attività di Ant o sfruttando l’integrazione predefinita con ScalaTest o JUnit. Vi consigliamo di usare JUnit se volete eseguire le specifiche in un IDE. Queste e altre opzioni sono descritte nella Guida per l’utente di Specs, disponibile all’indirizzo http://code.google.com/p/specs/wiki/RunningSpecs.

ScalaCheck

ScalaCheck [ScalaCheckTool] è una conversione in Scala dell’innovativo strumento Haskell QuickCheck [QuickCheck] che supporta il collaudo automatico basato sulla specifica, a volte chiamato collaudo di “proprietà” basato sul tipo nella letteratura sul linguaggio Haskell (per esempio, si veda [O’Sullivan2009]).

ScalaCheck può essere installato usando sbaz tramite il comando sbaz install scalacheck.

ScalaCheck (o QuickCheck per Haskell) vi permette di specificare, per un tipo, le condizioni che dovrebbero essere vere per qualsiasi istanza di quel tipo. Lo strumento genera automaticamente diverse istanze del tipo e verifica che le condizioni siano soddisfatte per quelle istanze.

Ecco un test ScalaCheck per il tipo Complex.

// esempi/cap-14/complex-check-script.scala

import org.scalacheck._
import org.scalacheck.Prop._

def toD(i: Int) = i * .1

implicit def arbitraryComplex: Arbitrary[Complex] = Arbitrary {
  Gen.sized {s =>
    for {
      r <- Gen.choose(-toD(s), toD(s))
      i <- Gen.choose(-toD(s), toD(s))
    } yield Complex(r, i)
  }
}

object ComplexSpecification extends Properties("Complex") {
  def additionTest(a: Complex, b: Complex) =
    (a + b).real.equals(a.real + b.real) &&
    (a + b).imaginary.equals(a.imaginary + b.imaginary)

  def subtractionTest(a: Complex, b: Complex) =
    (a - b).real.equals(a.real - b.real) &&
    (a - b).imaginary.equals(a.imaginary - b.imaginary)

  val zero = Complex(0.0, 0.0)

  specify("addizione di (0,0)", (a: Complex)  => additionTest(a, zero))
  specify("sottrazione di (0,0)", (a: Complex)  => subtractionTest(a, zero))

  specify("addizione", (a: Complex, b: Complex) => additionTest(a,b))
  specify("sottrazione", (a: Complex, b: Complex) => subtractionTest(a,b))
}
ComplexSpecification.check

La funzione toD converte semplicemente un Int in un Double moltiplicando per 0.1. Serve per convertire un indice intero fornito da ScalaCheck in valori reali che verranno usati per costruire istanze di Complex.

Abbiamo anche bisogno di una conversione implicita, visibile nell’ambito del test, che generi nuovi valori di Complex. La funzione arbitraryComplex ci fornisce questo generatore, restituendo un oggetto Arbitrary[Complex] che fa parte della API di ScalaCheck. Nel metodo di conversione, ScalaCheck invoca un altro metodo della propria API, Gen[Complex].sized, a cui forniamo un letterale funzione che assegna a una variabile s un valore intero passato dal sistema. Poi, il metodo di conversione usa un’espressione for per restituire numeri complessi con parti reale e immaginaria che vanno da -toD(s) a toD(s) (cioè da -(s * .1) a (s * .1)). Fortunatamente, non dovete definire conversioni implicite o generatori per la maggior parte dei tipi Scala e Java comunemente usati.

La parte più interessante è la definizione e l’uso di ComplexSpecification. Questo oggetto contiene alcuni metodi di utilità, additionTest e subtractionTest, che restituiscono true se la condizione da loro definita è vera. Per quanto riguarda additionTest, se un nuovo numero complesso è la somma di altri due numeri complessi allora la parte reale di questo numero deve essere uguale alla somma delle parti reali dei due numeri originali; allo stesso modo, una condizione simile deve verificarsi per la parte immaginaria dei numeri. Per quanto riguarda subtractionTest, le stesse condizioni devono verificarsi quando si opera una sottrazione anziché un’addizione.

Poi, due clausole specify asseriscono che le condizioni per l’addizione e la sottrazione dovrebbero verificarsi per ogni numero complesso quando Complex(0.0, 0.0) viene rispettivamente sommato o sottratto da quel numero. Due ulteriori clausole specify asseriscono che le condizioni devono anche verificarsi per qualsiasi coppia di numeri complessi.

Infine, quando ComplexSpecification.check viene invocato, i test vengono eseguiti più volte con valori differenti per i numeri complessi, verificando che le proprietà specificate siano valide per ogni combinazione di numeri passata ai metodi di utilità.

Possiamo eseguire il controllo usando il comando seguente (ancora una volta, stiamo ipotizzando che Complex sia già stato compilato nella directory build).

scala -classpath ../lib/scalacheck.jar:build complex-check-script.scala

L’esecuzione produce l’uscita seguente.

+ Complex.addizione di (0,0): OK, passed 100 tests.
+ Complex.addizione: OK, passed 100 tests.
+ Complex.sottrazione di (0,0): OK, passed 100 tests.
+ Complex.sottrazione: OK, passed 100 tests.

Notate che ScalaCheck ha usato 100 ingressi differenti per collaudare ogni singolo caso descritto dalle clausole specify.

È importante capire qual è l’utilità di ScalaCheck. Anziché scrivere un numero sufficiente di test di “esempio” su dati rappresentativi, che è un processo tedioso e soggetto a errori, definiamo “generatori” riusabili, come la funzione arbitraryComplex, per produrre un insieme appropriato di istanze del tipo sottoposto al collaudo. Poi specifichiamo le proprietà che dovrebbero valere per qualsiasi istanza, e ScalaCheck si occupa di verificare queste proprietà su un campione casuale di istanze prodotte dal generatore.

Potete trovare ulteriori esempi d’uso di ScalaCheck nell’archivio scaricabile che contiene gli esempi di codice. Alcuni dei tipi usati nel caso di studio del libro paga nella sezione DSL interni del capitolo 11 sono stati collaudati con ScalaCheck, anche se i test non sono stati mostrati.

Infine, notate che esiste un’altra conversione di QuickCheck chiamata Reductio, inclusa nel progetto [FunctionalJava]. Reductio è meno popolare di ScalaCheck, ma, insieme alla API Scala, offre anche una API Java “nativa”, quindi potrebbe rivelarsi più conveniente per gli sviluppatori che lavorano solamente in Java.

Altre librerie e strumenti degni di nota per Scala

Se da un lato Scala usufruisce del ricco patrimonio costituito dalle librerie esistenti per Java e .NET, dall’altro il numero delle librerie scritte specificamente per Scala è in costante aumento. Qui ne esamineremo alcune tra le più interessanti.

Lift

Lift (http://liftweb.net/) è il più importante framework per applicazioni web scritto in Scala e ha recentemente raggiunto la versione 1.0. Lift è stato usato per un certo numero di siti web commerciali. La documentazione di Lift si può trovare sul sito web indicato.

Tra gli altri framework web segnaliamo Sweet (http://code.google.com/p/sweetscala/), Pinky (http://bitbucket.org/pk11/pinky/) e Slinky (http://code.google.com/p/slinky2/).

Scalaz

Scalaz (http://code.google.com/p/scalaz/) è una libreria che cerca di colmare le lacune della libreria standard. Tra le sue funzionalità possiamo annoverare diversi miglioramenti a numerosi tipi fondamentali di Scala, come Boolean, Unit, String e Option, più il supporto per astrazioni di controllo funzionali, come FoldLeft, FoldRight, Monad, &c., che estendono quanto è disponibile nella libreria standard.

Scalax

Scalax (http://scalax.scalaforge.org/) è un’altra libreria di terze parti che tenta di completare la libreria standard di Scala.

MetaScala

MetaScala (http://www.assembla.com/wiki/show/metascala) è una libreria sperimentale di metaprogrammazione per Scala. Le funzioni di metaprogrammazione tendono a essere meno potenti nei linguaggi staticamente tipati rispetto ai linguaggi dinamicamente tipati. Inoltre, la JVM e il CLR di .NET impongono i propri vincoli alla metaprogrammazione.

Molte caratteristiche di Scala evitano la necessità della metaprogrammazione, se paragonate a quelle di linguaggi come Ruby, ma a volte la metaprogrammazione è ancora utile. MetaScala tenta di soddisfare questi bisogni in maniera più completa rispetto al supporto per la riflessione integrato in Scala.

JavaRebel

JavaRebel è uno strumento commerciale che permette di ricaricare dinamicamente le classi (scritte in qualsiasi linguaggio) su una JVM in esecuzione, superando i limiti del supporto fornito nativamente dalla funzione “HotSwap” della JVM. JavaRebel è progettato per offrire allo sviluppatore una risposta più veloce ai cambiamenti, presentando un’esperienza più simile alla rapidità di cui godono gli utenti dei linguaggi dinamici. JavaRebel può essere usato anche con codice Scala.

Varie librerie minori

Infine, ecco un elenco di altre librerie specifiche per Scala che potrebbero esservi utili per sviluppare le vostre applicazioni.

Tabella 14.5. Varie librerie Scala.

NomeDescrizione e URL

Kestrel

Un piccolo sistema molto veloce per gestire code. (http://github.com/robey/kestrel/).

ScalaModules

Un DSL Scala per semplificare lo sviluppo su OSGi (http://code.google.com/p/scalamodules/).

Configgy

Gestisce i file di configurazione e la registrazione delle attività per i “demoni” scritti in Scala (http://www.lag.net/configgy/).

scouchdb

Un’interfaccia Scala per CouchDB (http://code.google.com/p/scouchdb/).

Akka

Un progetto per realizzare una piattaforma su cui costruire applicazioni distribuite e tolleranti ai guasti basate su REST, attori, &c. (http://akkasource.org).

scala-query

Una API Scala per effettuare query su database in maniera type-safe (http://github.com/szeiger/scala-query/).

Esamineremo come usare Scala insieme a diverse librerie Java ben note dopo aver parlato, nella prossima sezione, della interoperabilità tra Scala e Java a livello di linguaggio.

Interoperabilità con Java

Scala è uno dei linguaggi alternativi per la JVM che si avvicinano di più a interagire con il codice sorgente Java senza soluzione di continuità. I dettagli sulla interoperabilità tra Scala e il codice scritto in Java esaminati in questa sezione, una volta compresi, possono essere generalizzati per applicarli al caso di altri linguaggi per la JVM, come JRuby o Groovy; per esempio, se sapete già come usare insieme JRuby e Java, e sapete come usare insieme Java e Scala, allora siete anche in grado di usare JRuby insieme a Scala.

Dato che la sintassi Scala è fondamentalmente un sovrainsieme della sintassi Java, in genere invocare codice Java da Scala è piuttosto semplice. Per andare nell’altra direzione è necessario capire il modo in cui alcune caratteristiche di Scala vengono codificate per soddisfare le specifiche della JVM. [Spiewak2009a] e [Odersky2008] arricchiscono con ulteriori dettagli l’analisi delle diverse questioni legate all’interoperabilità che esamineremo in questa sezione.

I generici in Java e Scala

Abbiamo visto numerosi esempi di codice Scala che usa tipi Java come java.lang.String e varie classi di collezione. Instanziare tipi generici Java in Scala è facile (a partire da Scala 2.7.0). Considerate la classe generica Java molto semplice riportata di seguito.

// esempi/cap-14/JStack.java

import java.util.*;

public class JStack<T> {
  private List<T> stack = new ArrayList<T>();
  public void push(T t) {
    stack.add(t);
  }
  public T pop() {
    return stack.remove(stack.size() - 1);
  }
}

Possiamo istanziare questa classe da Scala specificando il parametro di tipo come mostrato nel codice seguente.

// esempi/cap-14/JStack-spec.scala

import org.specs._

object JStackSpec extends Specification {
  "L'invocazione di un tipo generico Java da Scala" should {
    "supportare la parametrizzazione" in {
      val js = new JStack[String]
      js must notBe(null)  // controllo fittizio...
    }
    "supportare l'invocazione dei metodi del tipo" in {
      val js = new JStack[String]
      js.push("uno")
      js.push("due")
      js.pop() mustEqual "due"
      js.pop() mustEqual "uno"
    }
  }
}

A partire da Scala 2.7.2 potete anche usare i generici Scala da Java. Il seguente test per JUnit 4 mostra alcune idiosincrasie che potreste incontrare.

// esempi/cap-14/SMapTest.java

import org.junit.*;
import static org.junit.Assert.*;
import scala.*;
import scala.collection.mutable.LinkedHashMap;

public class SMapTest {
  static class Name {
    public String firstName;
    public String lastName;

    public Name(String firstName, String lastName) {
      this.firstName = firstName;
      this.lastName  = lastName;
    }
  }

  LinkedHashMap<Integer, Name> map;

  @Before
  public void setup() {
    map = new LinkedHashMap<Integer, Name>();
    map.update(1, new Name("Dean", "Wampler"));
    map.update(2, new Name("Alex", "Payne"));
  }

  @Test
  public void usingMapGetWithWarnings() {
    assertEquals(2, map.size());
    Option<Name> n1 = map.get(1);  // attenzione!
    Option<Name> n2 = map.get(2);  // attenzione!
    assertTrue(n1.isDefined());
    assertTrue(n2.isDefined());
    assertEquals("Dean", n1.get().firstName);
    assertEquals("Alex", n2.get().firstName);
  }

  @Test
  public void usingMapGetWithoutWarnings() {
    assertEquals(2, map.size());
    Option<?> n1 = map.get(1);
    Option<?> n2 = map.get(2);
    assertTrue(n1.isDefined());
    assertTrue(n2.isDefined());
    assertEquals("Dean", ((Name) n1.get()).firstName);
    assertEquals("Alex", ((Name) n2.get()).firstName);
 }
}

Su sistemi di tipo UNIX, questo test si può compilare con il seguente comando.

javac -Xlint:unchecked -cp $SCALA_HOME/lib/scala-library.jar:$JUNIT_HOME/junit-4.4.jar SMapTest.java

SCALA_HOME e JUNIT_HOME sono le directory di installazione di Scala e di JUnit, rispettivamente.

La classe SMapTest definisce una classe Name annidata che viene usata come tipo “valore” in un’istanza di scala.collection.mutable.LinkedHashMap. Per semplicità, Name è dotata di campi firstName e lastName pubblici e di un costruttore.

Il metodo setup crea una nuova istanza di LinkedHashMap<Integer,Name> e vi inserisce due coppie chiave-valore. I due test, usingMapGetWithWarnings e usingMapGetWithoutWarnings, esercitano l’interoperabilità tra Java e Scala nello stesso modo. Tuttavia, il primo test genera due messaggi di warning a tempo di compilazione, indicati dai commenti, mentre il secondo test viene compilato senza problemi.

SMapTest.java:29: warning: [unchecked] unchecked conversion
found   : scala.Option
required: scala.Option<SMapTest.Name>
    Option<Name> n1 = map.get(1);  // attenzione!
                             ^
SMapTest.java:30: warning: [unchecked] unchecked conversion
found   : scala.Option
required: scala.Option<SMapTest.Name>
    Option<Name> n2 = map.get(2);  // attenzione!
                             ^
2 warnings

I messaggi di avvertimento vengono generati a causa della cancellazione di tipo. Nella libreria Scala compilata, il tipo di ritorno di Map.get è Option senza nessun parametro di tipo, cioè effettivamente Option<Object>, perciò otteniamo un avvertimento in corrispondenza delle conversioni del tipo in Option<Name>.

Il secondo test, usingMapGetWithoutWarnings, non genera messaggi di avvertimento perché stiamo assegnando i valori restituiti da Map.get a Option<?>, per poi effettuare una conversione esplicita a Name quando invochiamo Option.get nelle ultime due asserzioni.

Usare le funzioni Scala in Java

Riprendendo l’esempio di SMapTest appena visto, possiamo vedere come invocare le funzioni Scala dal codice Java.

// esempi/cap-14/SMapTestWithFunctions.java

import org.junit.*;
import static org.junit.Assert.*;
import scala.*;
import scala.collection.mutable.LinkedHashMap;
import static scala.collection.Map.Projection;

public class SMapTestWithFunctions {
  static class Name {
    public String firstName;
    public String lastName;

    public Name(String firstName, String lastName) {
      this.firstName = firstName;
      this.lastName  = lastName;
    }

    public static Name emptyName = new Name("", "");

    public static Function0<Name> empty = new Function0<Name>() {
      public Name apply() { return emptyName; }

      public int $tag() { return 0; }
    };
  }

  LinkedHashMap<Integer, Name> map;

  @Before
  public void setup() {
    map = new LinkedHashMap<Integer, Name>();
    map.update(1, new Name("Dean", "Wampler"));
    map.update(2, new Name("Alex", "Payne"));
  }

  @Test
  public void usingMapGetOrElse() {
    assertEquals(2, map.size());
    assertEquals("Dean", ((Name) map.getOrElse(1, Name.empty)).firstName);
    assertEquals("Alex", ((Name) map.getOrElse(2, Name.empty)).firstName);
  }

  Function1<Integer, Boolean> filter = new Function1<Integer, Boolean>() {
    public Boolean apply(Integer i) { return i.intValue() <= 1; }

    public <A> Function1<A,Boolean> compose(Function1<A,Integer> g) {
      return Function1$class.compose(this, g);
    }

    public <A> Function1<Integer,A> andThen(Function1<Boolean,A> g) {
      return Function1$class.andThen(this, g);
    }

    public int $tag() { return 0; }
  };

  @Test
  public void usingFilterKeys() {
    assertEquals(2, map.size());
    Projection<Integer, Name> filteredMap =
        (Projection<Integer, Name>) map.filterKeys(filter);
    assertEquals(1, filteredMap.size());
    assertEquals("Dean", filteredMap.getOrElse(1, Name.empty).firstName);
    assertEquals("",     filteredMap.getOrElse(2, Name.empty).firstName);
  }
}

La classe SMapTestWithFunctions è dotata di una propria classe Name a cui aggiunge un oggetto statico emptyName e un oggetto statico di tipo scala.Function0 chiamato empty, che a sua volta definisce apply in modo da restituire emptyName. Notate come sia anche necessario definire il metodo $tag che è stato menzionato in precedenza nella sezione Gli strumenti scalap, javap e jad a riga di comando.

L’oggetto funzione empty è necessario per poter usare Map.getOrElse nel metodo di collaudo usingMapGetOrElse. La firma di getOrElse è la seguente:

def getOrElse[B2 >: B](key : A, default : => B2) : B2

Qui, A è il parametro di tipo per la chiave, B è il parametro di tipo per il valore e B2 è un supertipo di B o lo stesso tipo B. Il secondo argomento default è un parametro per nome, dei quali abbiamo già parlato nel capitolo 8. Notate che i parametri per nome sono implementati come oggetti scala.Function0, quindi non possiamo semplicemente passare l’oggetto statico emptyName.

Il secondo test, usingFilterKeys, richiede un oggetto Function1, dotato di un metodo apply che accetta un argomento. Usiamo questo oggetto Function1 come un filtro passato a Map.filterKeys.

Definiamo il filtro prima del test. In questo caso, il codice Java è considerevolmente più complicato di quanto sarebbe il codice Scala equivalente: non solo dobbiamo definire i metodi apply e $tag, ma dobbiamo anche definire i metodi compose e andThen usati per la composizione di funzioni. Fortunatamente, possiamo delegare le operazioni agli oggetti che fanno già parte della libreria Scala, come mostrato. Notate che gli altri tipi FunctionN, per N che va da 2 a 22, hanno altri metodi che avremmo dovuto implementare usando simile codice “stereotipato”; per esempio, ognuno di questi tipi è dotato del proprio metodo curry.

Infine, ricordatevi che nella sezione Gli oggetti associati e i metodi statici di Java del capitolo 6 abbiamo evidenziato che i metodi definiti negli oggetti associati non vengono visti come metodi statici dal codice Java. Per esempio, i metodi main definiti negli oggetti associati non possono essere usati per eseguire applicazioni, perciò dovreste definire tali metodi in oggetti singleton.

Quindi, può essere difficile usare gli oggetti funzione di Scala. Nel caso abbiate bisogno di usarli frequentemente, potreste definire alcune classi Java di utilità che gestiscano il codice “stereotipato” per tutti i metodi a parte apply.

Proprietà JavaBeans

Nel capitolo 5 abbiamo visto che Scala non segue le convenzioni JavaBeans [JavaBeansSpec] per i metodi di lettura e scrittura dei campi, per ragioni descritte nella sezione Quando i metodi di accesso e i campi sono indistinguibili: il principio di accesso uniforme. Tuttavia, in certe occasioni i metodi di accesso nello stile JavaBeans potrebbero rivelarsi necessari, per esempio quando volete che le vostre istanze Scala risultino configurabili tramite un meccanismo di iniezione di dipendenza come quello fornito dal framework Spring [SpringFramework], oppure quando volete sfruttare l’“introspezione” dei componenti effettuata da alcuni IDE.

Scala risolve questo problema con l’annotazione @scala.reflect.BeanProperty, che potete applicare ai campi per indurre il compilatore a generare metodi di lettura e scrittura nello stile JavaBeans. Vi abbiamo presentato questa annotazione nella sezione Annotazioni del capitolo 13.

Ricordate la classe Complex che abbiamo visto in precedenza? Ora aggiungiamo l’annotazione a tutti i parametri del costruttore, che in una classe case rappresentano i campi.

// esempi/cap-14/complex-javabean.scala

case class ComplexBean(
  @scala.reflect.BeanProperty real: Double,
  @scala.reflect.BeanProperty imaginary: Double) {

  def +(that: ComplexBean) =
    new ComplexBean(real + that.real, imaginary + that.imaginary)
  def -(that: ComplexBean) =
    new ComplexBean(real - that.real, imaginary - that.imaginary)
}

Se compilate questa classe e poi la decompilate con javap -classpath … ComplexBean, ottenete il risultato seguente.

public class ComplexBean extends java.lang.Object
  implements scala.ScalaObject,scala.Product,java.io.Serializable {
  public ComplexBean(double, double);
  public java.lang.Object productElement(int);
  public int productArity();
  public java.lang.String productPrefix();
  public boolean equals(java.lang.Object);
  public java.lang.String toString();
  public int hashCode();
  public int $tag();
  public ComplexBean $minus(ComplexBean);
  public ComplexBean $plus(ComplexBean);
  public double imaginary();
  public double real();
  public double getImaginary();
  public double getReal();
}

Ora confrontate questo risultato con la decompilazione del file Complex.class originale.

public class Complex extends java.lang.Object
  implements scala.ScalaObject,scala.Product,java.io.Serializable {
  public Complex(double, double);
  public java.lang.Object productElement(int);
  public int productArity();
  public java.lang.String productPrefix();
  public boolean equals(java.lang.Object);
  public java.lang.String toString();
  public int hashCode();
  public int $tag();
  public Complex $minus(Complex);
  public Complex $plus(Complex);
  public double imaginary();
  public double real();
}

Quando eseguite javap su questi file, l’ordine in cui vengono mostrati i metodi potrebbe essere differente. Qui, i metodi sono stati riordinati in modo che i due elenchi corrispondano quanto più è possibile. Notate che le sole differenze sono nei nomi delle classi e nella presenza dei metodi getImaginary e getReal nella classe ComplexBean. Se i campi real e imaginary fossero stati dichiarati come var anziché val, sarebbero comparsi anche i corrispondenti metodi di scrittura.

La pagina Scaladoc per @BeanProperty (nella versione 2.7) dice che non potete invocare i metodi di scrittura dei campi da Scala. In realtà potete farlo, ma, come suggerito nella pagina Scaladoc, dovreste creare i metodi di scrittura (e di lettura) seguendo le convenzioni Scala anziché le convenzioni JavaBeans.

I tipi AnyVal e i tipi primitivi in Java

Avrete notato che, nell’esempio precedente, le istanze di Double presenti in Complex sono state convertite in valori primitivi double di Java. In effetti, tutti i tipi AnyVal vengono convertiti nei loro corrispondenti tipi Java primitivi, come illustrato nella tabella 7.3, dove, in particolare, abbiamo constatato che Unit corrisponde a void.

Nomi Scala nel codice Java

Come abbiamo detto nel capitolo 3, Scala ammette identificatori più flessibili rispetto a Java, per esempio consentendo di usare caratteri operatore come *, <, &c. nei nomi. Questi caratteri vengono codificati (o “trasformati”, se preferite) per soddisfare i vincoli più stretti imposti sui nomi dalla JVM. I caratteri speciali vengono tradotti come segue (la tabella è adattata da [Spiewak2009a]).

Tabella 14.6. Codifica dei caratteri operatore.

OperatoreCodifica

=

$eq

>

$greater

<

$less

+

$plus

-

$minus

*

$times

/

$div

\

$bslash

|

$bar

!

$bang

?

$qmark

:

$colon

%

$percent

^

$up

&

$amp

@

$at

#

$hash

~

$tilde

Potete vedere la codifica all’opera grazie al tratto seguente, in cui ogni carattere viene usato per dichiarare un metodo astratto che non accetta argomenti e restituisce Unit.

// esempi/cap-14/all-op-chars.scala

trait AllOpChars {
  def == : Unit   // $eq$eq
  def >  : Unit   // $greater
  def <  : Unit   // $less
  def +  : Unit   // $plus
  def -  : Unit   // $minus
  def *  : Unit   // $times
  def /  : Unit   // $div
  def \  : Unit   // $bslash
  def |  : Unit   // $bar
  def !  : Unit   // $bang
  def ?  : Unit   // $qmark
  def :: : Unit   // $colon$colon
  def %  : Unit   // $percent
  def ^  : Unit   // $up
  def &  : Unit   // $amp
  def @@ : Unit   // $at$at
  def ## : Unit   // $hash$hash
  def ~  : Unit   // $tilde
}

Notate che abbiamo raddoppiato alcuni caratteri per consentirne la compilazione come nomi di metodo, nei casi in cui usare un singolo carattere sarebbe risultato ambiguo. Se compilate questo file e poi decompilate il file di classe risultante con javap AllOpChars otterrete la seguente interfaccia Java. (Abbiamo riarrangiato l’ordine dei metodi per farlo combaciare con l’ordine dei metodi nel file Scala originale.)

Compiled from "all-op-chars.scala"
public interface AllOpChars{
  public abstract void $eq$eq();
  public abstract void $greater();
  public abstract void $less();
  public abstract void $plus();
  public abstract void $minus();
  public abstract void $times();
  public abstract void $div();
  public abstract void $bslash();
  public abstract void $bar();
  public abstract void $bang();
  public abstract void $qmark();
  public abstract void $colon$colon();
  public abstract void $percent();
  public abstract void $up();
  public abstract void $amp();
  public abstract void $at$at();
  public abstract void $hash$hash();
  public abstract void $tilde();
}

Per concludere, l’interoperabilità tra Java e Scala è molto elevata, ma ci sono alcuni dettagli che dovreste tenere a mente quando invocate codice Scala da Java. Se non ricordate il modo in cui un identificatore Scala viene codificato o un metodo Scala viene trasformato in bytecode valido, usate javap per scoprirlo.

Interoperabilità con le librerie Java

In questa sezione considereremo l’interoperabilità tra Scala e alcuni framework Java piuttosto diffusi: nello specifico, parleremo di AspectJ, Spring, Terracotta e Hadoop. Dato che sono largamente usati nelle applicazioni “aziendali” e di rete, è particolarmente importante che Scala riesca a interagire con questi framework in maniera efficace.

AspectJ

AspectJ [AspectJ] è un’estensione di Java che supporta la programmazione orientata agli aspetti (AOP), nota anche come sviluppo del software orientato agli aspetti [AOSD]. La AOP ha lo scopo di abilitare modifiche sistemiche dello stesso genere attraverso molti moduli, evitando nel contempo di copiare e incollare lo stesso codice più volte in ogni modulo. Evitare questa duplicazione significa incrementare la produttività e ridurre enormemente il numero di difetti nel software.

Per esempio, se volete che tutte le modifiche ai campi di tutti gli oggetti appartenenti al “modello del dominio” siano rese automaticamente persistenti dopo che i cambiamenti si sono verificati, potete programmare un aspetto che osserva quei cambiamenti e innesca una scrittura persistente dopo ogni modifica.

AspectJ supporta la AOP offrendo un linguaggio a punti di taglio (in inglese, pointcut) per specificare in maniera dichiarativa tutti i “punti di esecuzione” per i quali viene richiesta una particolare modifica (chiamata consiglio, dal termine inglese advice) nel comportamento di un programma. Nella terminologia di AspectJ, ogni punto di esecuzione viene chiamato punto di unione e una particolare interrogazione sui punti di unione è un punto di taglio. Quindi, il linguaggio di AspectJ è una specie di linguaggio di interrogazione. Per un dato punto di taglio, AspectJ incorpora le modifiche comportamentali desiderate in ogni punto di unione individuato dal punto di taglio, rendendo facoltativo l’inserimento manuale di queste modifiche. Un aspetto incapsula i punti di taglio e i consigli, in modo simile a come una classe incapsula campi e metodi.

Per una introduzione dettagliata ad AspectJ, corredata di molti esempi pratici, si veda [Laddad2009].

Ci sono due questioni da considerare quando si usa AspectJ con Scala: come fare riferimento ai punti di esecuzione definiti in Scala (per esempio, metodi e tipi Scala) usando il linguaggio a punti di taglio di AspectJ, e come invocare codice Scala sotto forma di consiglio.

Cominceremo implementando un aspetto che registra le invocazioni di metodo sulla classe Complex già usata nelle sezioni precedenti. Questa volta aggiungeremo alla classe una dichiarazione di package, per creare un nuovo ambito di visibilità.

// esempi/cap-14/aspectj/complex.scala

package example.aspectj

case class Complex(real: Double, imaginary: Double) {
  def +(that: Complex) =
    new Complex(real + that.real, imaginary + that.imaginary)
  def -(that: Complex) =
    new Complex(real - that.real, imaginary - that.imaginary)
}

Ecco un object che usa questa classe Complex.

// esempi/cap-14/aspectj/complex-main.scala

package example.aspectj

object ComplexMain {
  def main(args: Array[String]) {
    val c1 = Complex(1.0, 2.0)
    val c2 = Complex(3.0, 4.0)
    val c12 = c1 + c2
    println(c12)
  }
}

Di seguito, ecco un aspetto AspectJ che definisce un punto di taglio per la creazione di istanze di Complex e un altro punto di taglio per l’invocazione del metodo +.

// esempi/cap-14/aspectj/LogComplex.aj

package example.aspectj;

public aspect LogComplex {
  public pointcut newInstances(double real, double imag):
    execution(Complex.new(..)) && args(real, imag);

  public pointcut plusInvocations(Complex self, Complex other):
    execution(Complex Complex.$plus(Complex)) && this(self) && args(other);

  before(double real, double imag): newInstances(real, imag) {
    System.out.println("new Complex(" + real + "," + imag + ") invocato.");
  }

  before(Complex self, Complex other): plusInvocations(self, other) {
    System.out.println("Invoco " + self + ".+(" + other + ")");
  }

  after(Complex self, Complex other) returning(Complex c):
    plusInvocations(self, other) {
    System.out.println("Complex.+ ha restituito " + c);
  }
}

Eviteremo di spiegare la sintassi AspectJ in maniera esaustiva, rimandandovi ai documenti [AspectJ] e [Laddad2009] se siete interessati ai dettagli; in questa sede ci limiteremo a presentare questo aspetto dal punto di vista “concettuale”.

Il primo pointcut, chiamato newInstances, corrisponde alle “esecuzioni” del costruttore, usando la sintassi Complex.new per fare riferimento al costruttore. Ci aspettiamo che ogni invocazione del costruttore riceva argomenti di tipo double poiché, come abbiamo visto in precedenza, le occorrenze di scala.Double vengono convertite al tipo primitivo double di Java durante la generazione del bytecode. La clausola args “lega” i valori degli argomenti passati in modo da permetterci di fare riferimento a essi nel consiglio.

Il secondo pointcut, chiamato plusInvocations, corrisponde alle “esecuzioni” del metodo +, che in effetti si chiama $plus nel bytecode. I parametri self e other vengono rispettivamente legati all’oggetto su cui il metodo + è stato invocato (usando la clausola this) e all’argomento passato (usando la clausola args).

Il primo consiglio before viene eseguito per il punto di taglio newInstances, cioè prima di entrare effettivamente nel costruttore. Registriamo l’invocazione mostrando i valori reale e immaginario passati come argomenti.

Il consiglio before successivo viene eseguito per il punto di taglio plusInvocations, cioè prima che il metodo + venga eseguito. Registriamo il valore di self (cioè l’istanza this) e l’altro numero.

Infine, viene eseguito anche un consiglio after returning per il punto di taglio plusInvocations, cioè dopo che il metodo + si è concluso. Catturiamo il valore di ritorno nella variabile c e lo registriamo.

Se avete installato AspectJ nella directory aspectj-home, potete compilare questo file come segue.

ajc -classpath .:aspectj-home/lib/aspectjrt.jar:../lib/scala-library.jar aspectj/LogComplex.aj

Per eseguire questo codice con l’aspetto LogComplex, useremo un sistema di introduzione a tempo di caricamento (http://www.eclipse.org/aspectj/doc/released/devguide/ltw.html). Invocheremo Java con un agente (chiamato weaver) che “introduce” il consiglio da LogComplex in Complex. Per usare l’introduzione a tempo di caricamento ci occorrerà anche il file di configurazione META-INF/aop.xml che segue.

<!-- esempi/cap-14/META-INF/aop.xml -->

<aspectj>
  <aspects>
    <aspect name="example.aspectj.LogComplex" />
    <include within="example.aspectj.*" />
  </aspects>

  <weaver options="-verbose">
    <dump within="example.aspectj.*" beforeandafter="true">
      <include within="example.aspectj.*" />
    </dump>
  </weaver>
</aspectj>

La directory META-INF dovrebbe essere nel percorso di ricerca delle classi; supporremo che si trovi nella directory di lavoro corrente. Questo file dice al sistema quale aspetto usare (il tag aspect), in quali classi introdurre l’aspetto (il tag include) e aumenta il livello di dettaglio nei messaggi informativi in uscita, per facilitare le attività di debug. Infine, possiamo eseguire l’applicazione con il comando seguente. (Il comando va digitato su un’unica riga; qui abbiamo usato il carattere \ per indicare un ritorno a capo utile ad adattare il comando alla larghezza della pagina.)

java -classpath .:aspectj-home/lib/aspectjrt.jar:../lib/scala-library.jar \
                -javaagent:aspectj-home/lib/aspectjweaver.jar example.aspectj.ComplexMain

Otterrete diversi messaggi che indicano la registrazione del processo di introduzione. L’uscita si conclude con queste righe.

new Complex(1.0,2.0) invocato.
new Complex(3.0,4.0) invocato.
Invoco Complex(1.0,2.0).+(Complex(3.0,4.0))
new Complex(4.0,6.0) invocato.
Complex.+ ha restituito Complex(4.0,6.0)
Complex(4.0,6.0)

Tutte le righe tranne l’ultima sono state stampate da LogComplex. Abbiamo aggiunto questo comportamento addizionale alla classe Complex senza inserire manualmente le istruzioni in quella classe!

Se ricordate, abbiamo detto che, quando usate AspectJ, potreste incontrare un secondo problema, relativo al modo di invocare codice Scala dall’interno di un consiglio. Nel nostro aspetto LogComplex, le istruzioni contenute nei diversi consigli before e after sono effettivamente scritte solo in Java. Di conseguenza, possiamo invocare codice Scala altrettanto facilmente applicando le stesse tecniche che abbiamo già imparato per invocare codice Scala da Java.

I tratti Scala sostituiscono quasi completamente gli aspetti. Nel capitolo 4 e nel capitolo 13 abbiamo visto come potete costruire tratti che modificano il comportamento di altri tratti e poi mescolare tra loro i comportamenti quando create nuove classi o nuove istanze. Questa tecnica potente vi permette di implementare una forma di consiglio per un aspetto. Tuttavia, Scala non possiede un linguaggio per dichiarare i punti di taglio come AspectJ. Vi serviranno le capacità di AspectJ se dovete influenzare un insieme di punti di unione che non condividono uno stesso supertipo. Tuttavia, quando vi trovate in questa situazione, dovreste considerare la possibilità di riorganizzare il vostro codice in modo da estrarre un tratto comune che fornisca gli “agganci” necessari a implementare un consiglio usando i tratti.

Spring

Spring [SpringFramework] è un framework Java open source molto diffuso nelle aziende, organizzato in diversi moduli che offrono una API realizzata completamente in Java per la AOP, un supporto integrato per AspectJ, un contenitore per la iniezione di dipendenza (in inglese, dependency injection o DI), API uniformi e ben progettate per invocare una varietà di altre API Java di terze parti, e componenti aggiuntivi per la sicurezza, lo sviluppo di applicazioni web, &c.

Qui ci concentreremo sulla iniezione di dipendenza, in quanto i problemi di interoperabilità con le altre parti di Spring si riducono a problemi riguardanti Java o AspectJ, di cui abbiamo già parlato.

Abbiamo esaminato il concetto di DI nella sezione L’iniezione di dipendenza in Scala: il pattern Cake del capitolo 13, dove abbiamo usato lo stesso linguaggio Scala per illustrare alcuni pattern eleganti per l’iniezione di dipendenza. Tuttavia, se vi trovate in un ambiente misto Java/Scala, potrebbe essere necessario gestire le dipendenze usando un framework DI come quello fornito da Spring.

Nella DI di Spring le dipendenze vengono specificate usando una combinazione di file di configurazione XML e annotazioni nel codice sorgente. La API di Spring risolve queste dipendenze nel momento in cui le classi vengono istanziate. Spring si aspetta che queste classi seguano le convenzioni JavaBeans (si veda [JavaBeansSpec]). Le classi ben progettate dipenderanno solamente dalle astrazioni, cioè da interfacce Java o tratti Scala, e le istanze concrete che soddisfano quelle dipendenze verranno passate ai componenti JavaBeans come argomenti di un costruttore o attraverso i metodi per impostare i valori. Quindi, quando usate la DI di Spring con le classi Scala, dovrete fare ricorso all’annotazione @scala.reflect.BeanProperty se l’iniezione avviene tramite i metodi per impostare i valori. L’annotazione non è necessaria se l’iniezione avviene tramite i costruttori.

Privilegiate l’iniezione attraverso i costruttori, quando è possibile. Questa scelta non solo elimina il bisogno di usare l’annotazione @BeanProperty, ma lascia ogni istanza in uno stato noto e valido quando il processo di costruzione termina.

Tuttavia, se iniettate dipendenze in un object Scala, l’iniezione dovrà avvenire tramite i metodi per impostare i valori, dato che non avete modo di definire i parametri del costruttore e non avete il controllo sul processo di costruzione.

Ricordatevi anche che Spring si aspetta nomi compatibili con Java, quindi dovete usare nomi codificati per i metodi e gli oggetti, nel caso sia necessario.

Ecco un esempio che illustra come “collegare” oggetti tra loro tramite Spring.

// esempi/cap-14/spring/object-bean.scala

package example.spring

case class NamedObject(name: String)

trait Factory {
  @scala.reflect.BeanProperty
  var nameOfFactory = "sconosciuto"

  def make(name: String): AnyRef
}

object NamedObjectFactory extends Factory {
  def make(name: String) = NamedObject(name)
}

case class FactoryUsingBean(factory: Factory)

La classe case FactoryUsingBean è un semplice tipo con una dipendenza da un’astrazione Factory che vogliamo iniettare attraverso un costruttore.

Il tratto Factory definisce l’astrazione: è dotato di un metodo make per creare istanze di qualche tipo. Lo abbiamo dotato anche di un campo nameOfFactory in modo da mostrare l’iniezione di dipendenza attraverso i metodi per impostare i valori sugli object, perché il sottotipo concreto che useremo effettivamente, NamedObjectFactory, è un object.

Scala ci obbliga a inizializzare nameOfFactory con un valore, ma useremo Spring per impostare il valore reale del campo. Dobbiamo usare l’annotazione @BeanProperty per generare il metodo setNameOfFactory che Spring si aspetta di trovare.

Il metodo concreto make in NamedObjectFactory crea una nuova istanza di NamedObject, una semplice classe case con un campo name.

Notate che nessuno di questi tipi dipende dalla API Spring. Potete compilare questo file senza ricorrere ad alcun file JAR di Spring.

Successivamente, definiamo il “collegamento” tra le dipendenze usando un file di configurazione XML standard di Spring.

<!-- esempi/cap-14/spring/scala-spring.xml -->

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

  <bean id="factory" class="example.spring.NamedObjectFactory$">
    <property name="nameOfFactory" value="Factory per istanze di NamedObject" />
  </bean>

  <bean id="factoryUsingBean" class="example.spring.FactoryUsingBean">
    <constructor-arg ref="factory" />
  </bean>
</beans>

Definiamo due componenti bean. Il primo è la nostra factory, che dotiamo di un identificatore factory. La “classe” del componente in realtà è l’object NamedObjectFactory. Notate che dobbiamo aggiungere $ in coda al nome, per identificare il nome reale dell’oggetto nel bytecode.

Il tag property imposta il valore di nameOfFactory. Non possiamo controllare la creazione dell’istanza di un object, quindi dobbiamo iniettare la dipendenza corretta dopo che il processo di costruzione è terminato.

Il secondo componente è il nostro semplice FactoryUsingBean. Dato che questa è una classe, possiamo usare l’iniezione attraverso il costruttore. Il tag constructor-arg specifica che il componente factory viene usato per soddisfare la dipendenza al momento della costruzione.

Infine, ecco uno script che sfrutta questi tipi per fare una dimostrazione di come si usa la DI di Spring in Scala.

// esempi/cap-14/spring/object-bean-script.scala

import example.spring._
import org.springframework.context.support._

val context = new ClassPathXmlApplicationContext("spring/scala-spring.xml");

val bean = context.getBean("factoryUsingBean").asInstanceOf[FactoryUsingBean]
println("Nome della factory: " + bean.factory.nameOfFactory)

val obj  = bean.factory.make("Dean Wampler")
println("Oggetto: " + obj)

Creiamo un’istanza di ClassPathXmlApplicationContext, specificando il nostro file XML. Questo oggetto rappresenta il contesto attraverso il quale possiamo accedere al contenitore DI. Gli chiediamo la nostra factoryUsingBean. Dobbiamo convertire l’oggetto AnyRef restituito (cioè un Object Java) nel tipo corretto. Stampiamo il nome della factory, per vedere se è quello giusto.

Poi chiediamo alla factory di costruire “qualcosa” con la stringa "Dean Wampler". Quando stampiamo l’oggetto restituito, dovremmo vedere un’istanza di NamedObject.

Se avete installato Spring nella directory spring-home, potete eseguire questo script con il comando seguente. (Notate che è necessario includere la directory di lavoro corrente . nel percorso di ricerca delle classi per trovare il file XML.)

scala -cp spring-home/dist/spring.jar:spring-home/.../commons-logging.jar:. spring/object-bean-script.scala

Le informazioni che riceverete in uscita sono molte, ma a noi interessano solo le ultime due righe.

…
Nome della factory: Factory per istanze di NamedObject
Oggetto: NamedObject(Dean Wampler)

Per far funzionare questo esempio è stato necessario creare un certo numero di file e impostare diversi dettagli di configurazione. Tutta questa fatica si può giustificare nel caso di un’applicazione Java di dimensioni moderatamente grandi. Tuttavia, Scala vi mette a disposizione tecniche nuove e più semplici per implementare l’iniezione di dipendenza senza usare file di configurazione e un contenitore DI.

Terracotta

Terracotta [Terracotta] è un prodotto open source che distribuisce un’applicazione su diversi server effettuando il clustering (letteralmente, raggruppamento) delle JVM su cui l’applicazione viene eseguita. Per ragioni di efficienza, non tutti gli oggetti dell’applicazione mantenuti in memoria vengono distribuiti; invece, è il programmatore che specifica nei file di configurazione quali sono le strutture dati da distribuire. Terracotta offre il vantaggio di non imporre modifiche al codice dell’applicazione per supportare questa distribuzione (almeno in linea di principio, poiché alcune personalizzazioni limitate possono essere utili a migliorare le prestazioni); invece, il supporto viene predisposto direttamente nel bytecode. Terracotta rappresenta un’alternativa alle cache distribuite che richiedono di modificare il codice dell’applicazione.

[Bonér2008a] offre un resoconto dettagliato di come usare Terracotta con gli attori Scala. Come prima cosa, è necessario installare un modulo di integrazione con Terracotta (TIM) specifico per Scala. Quando configurate gli oggetti da distribuire, dovete usare i nomi codificati per indicare gli oggetti associati, i metodi, &c. nella forma in cui esistono a livello di bytecode. Abbiamo parlato di queste codifiche nella sezione Nomi Scala nel codice Java più indietro in questo capitolo. Infine, dovete aggiungere alcuni parametri ulteriori all’invocazione del comando java contenuta nello script scala. Per il resto, il clustering di applicazioni Scala con Terracotta funziona esattamente come per le applicazioni Java.

Hadoop

MapReduce è un modello di programmazione dividi-e-conquista per elaborare grandi quantità di dati in parallelo. Nella fase di “mappatura”, un insieme di dati viene diviso in N sottoinsiemi di dimensioni approssimativamente uguali, dove N viene scelto per ottimizzare la quantità di lavoro che può essere compiuta in parallelo. Per esempio, N potrebbe essere vicino al numero totale di processori disponibili. (Alcuni processori potrebbero essere lasciati inattivi come “riserve” o dedicati ad altre elaborazioni.) La computazione desiderata viene eseguita su ogni sottoinsieme. La fase di “riduzione” combina i risultati dei calcoli eseguiti sui sottoinsiemi in un risultato finale.

Notate che la mappatura e la riduzione sono sostanzialmente operazioni funzionali. Quindi un linguaggio funzionale come Scala è l’ideale per scrivere applicazioni basate sul modello MapReduce.

I framework che implementano il modello MapReduce offrono strumenti per mappare e ridurre insiemi di dati, gestire i nodi di elaborazione e tutte le fasi della computazione, riavviare le operazioni che falliscono per qualche ragione, &c. L’utente di un framework basato su MapReduce deve solo scrivere gli algoritmi per la mappatura (suddivisione) dei dati in ingresso, per le computazioni con i sottoinsiemi di dati e per la riduzione dei risultati. Si veda [MapReduceTutorial] per una breve introduzione e [MapReduce] per una descrizione del framework MapReduce di Google, il cui nome è diventato lo standard de facto per questi framework.

Hadoop [Hadoop] è un framework open source creato e mantenuto da Yahoo! che si basa sul modello MapReduce. Esistono due librerie Scala che racchiudono la API di Hadoop: SHadoop (si veda [SHadoop]) e SMR (si vedano [SMRa] e [SMRb]). Entrambi gli esempi mostrano una grande riduzione nelle dimensioni del codice quando si usa Scala. [SMRa] attribuisce questa diminuzione al supporto di Scala per le funzioni di ordine superiore e per le funzioni anonime, al suo sofisticato sistema di tipi e all’inferenza di tipo, e alla capacità delle espressioni for di generare mappe in modo elegante e conciso.

Riepilogo, e poi?

In questo capitolo vi abbiamo presentato informazioni dettagliate sugli strumenti Scala a riga di comando che userete quotidianamente. Abbiamo anche esaminato il supporto per Scala disponibile in vari editor di testo e IDE, un certo numero di librerie importanti come per esempio le API di collaudo, e infine l’interoperabilità tra Scala e altri linguaggi e librerie per la JVM.

Questo completa la nostra rassegna sul mondo della programmazione in Scala. Il prossimo capitolo contiene un glossario di termini che abbiamo usato nel corso del libro, seguito da una lista di riferimenti utili per chi volesse approfondire ulteriormente le proprie conoscenze.


  1. [NdT] Questo repository non esiste più. Nel giugno 2010, Phillips ha cominciato a lavorare su un nuovo bundle, i cui sorgenti sono disponibili pubblicamente all’indirizzo http://github.com/paulp/scala.tmbundle.
  2. [NdT] Ma, a questo proposito, si veda la nota 1 del capitolo 3.

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