Il linguaggio Scala

Da tempo si sente l’esigenza di superare Java per rendere la
programmazione più flessibile, agile e, se possibile, vicina al
linguaggio naturale. Molti hanno individuato la ragione della rigidità
di Java nella sua tipizzazione statica e si sono pertanto rivolti a
linguaggi dinamici. Tali strumenti di programmazione, però, suscitano
href="http://www.francolombardo.net/linguaggi-statici-e-dinamici-paure-o-problemi_post-6.html">numerosi
dubbi e perplessità, in chi, come me, proviene dal mondo della
tipizzazione statica.

È per questo che il nuovo linguaggio href="http://www.scala-lang.org/">Scala, tipizzato staticamente e
libero dai tanti problemi di Java, non manca di affascinarmi.
Realizzato a partire dal 2001 dal Politecnico di Losanna sotto la
guida di Martin
Odersky
, uno degli sviluppatori del compilatore Java, è stato
rilasciato pubblicamente per la prima volta nel 2004 in due versioni,
una per la piattaforma Java ed un’altra per .NET, ed ha subito un
sostanziale miglioramento nel corso del 2006. Vediamone alcune delle
caratteristiche principali.


Interoperabilità con Java

La sintassi di base del linguaggio è piuttosto simile a quella di
Java. È vero che Scala è sintatticamente molto ricco (per alcuni
troppo) ed è possibile creare con esso delle espressioni alquanto
criptiche, ma un programmatore disciplinato ha tutti gli strumenti per
scrivere codice leggibile ed espressivo.

Compilare un programma Scala significa generare dei normalissimi file
.class che contengono bytecode interpretabile
da una qualsiasi JVM 1.4
o superiore. Questi file possono essere eseguiti tramite il comando
scala, che altro non è se non un file batch
per lanciare l’ambiente di
esecuzione Scala all’interno della JVM.

Le librerie Java possono essere importate in Scala, anzi il package
java.lang viene importato di default. Inoltre da Scala si
possono estendere classi o implementare interfacce Java, il che ne
consente l’utilizzo all’interno dei più comuni framework. Sebbene in
alcuni casi siano stati creati dei meccanismi per l’integrazione di
librerie Java all’interno di linguaggi dinamici e viceversa, le
soluzioni adottate non mi sembrano così lineari come quelle di Scala,
che è stato disegnato sin dal principio con questo obiettivo. Ma qui,
ovviamente, si tratta di sensibilità personale. Per illustrare
meglio quanto appena detto, è giunta l’ora di dare spazio al
protagonista inevitabile di ogni introduzione ad un linguaggio: Hello
World!

import java.util.Date

object hello extends Application { 
  println("Yet Another Hello World Program")
  println("running on " + new Date())  //Classe java visibile tramite import
  println("JVM version " + System.getProperty("java.version"))  //Classe di java.lang
}

Il codice può essere compreso immediatamente da un qualsiasi
programmatore Java. Notiamo solamente che defininendo un
object creiamo un Singleton, ovvero una classe
con una singola istanza.

Linguaggio OO e Funzionale

Scala vuole fondere in uno stesso linguaggio i paradigmi della
programmazione Object Oriented e di quella funzionale. Ogni elemento
del linguaggio è un oggetto, inclusi numeri e funzioni. Queste ultime,
pertanto, possono venire memorizzate in variabili, essere passate come
parametri, rappresentare il risultato di una chiamata di metodo,
oppure essere estese tramite ereditarietà. D’altro canto, se ogni
valore è un oggetto, ogni operazione è l’invocazione di un metodo.
L’espressione a(i) = 5 + 4 per assegnare il risultato di
una somma ad un elemento di un array viene infatti tradotta in
successivi invii di messaggi: a.update(i, 5.+(4)) Dai
linguaggi funzionali Scala acquisisce “closures”, funzioni di ordine
superiore (higher order functions), valutazione lazy dei
parametri e la predilezione per le strutture dati “immutabili” o href="http://en.wikipedia.org/wiki/Algebraic_data_type">tipi di dati
algebrici, come le liste. Tali strutture sono manipolabili tramite
filtri, mappature e folding. Per dare un assaggio di questi
concetti, vediamo il codice per risolvere il primo dei quesiti del href="http://projecteuler.net/">Progetto Eulero: trovare la somma
di tutti i multipli di 3 o di 5 inferiori a 1000.

object euler01 extends Application {
  println((1 until 1000)
                   .filter(n => n % 3 == 0 || n % 5 == 0)
                               .foldLeft(0)((accum, item) => accum + item))  
}

Qui la diversità da Java diviene più marcata. Spiegiamo il
programma nel dettaglio. Con l’espressione 1 until 1000
si applica il metodo until all’oggetto 1 di
classe Int passando come argomento 1000. Ciò
genera una sequenza (qualcosa di simile ad una Collection
Java) di interi. In realtà il metodo untilnon è definito
nella classe Int, ma in RichInt; viene qui
utilizzata una conversione impicita, che illustreremo più avanti.
Dalla sequenza vengono estratti i valori multipli di 3 o 5 passando al
metodo filter una funzione anonima che ottiene un booleano partendo da
un’intero. La sottosequenza cosi’ ottenuta viene ridotta ad un singolo
valore tramite il metodo foldLeft, il quale applica la
funzione passata come secondo argomento, nell’esempio la somma, in
maniera cumulativa a tutti gli elementi da sinistra a destra. Il primo
argomento di foldLeft è invece il valore iniziale
dell’accumulatore, nel nostro caso 0.

Inferenza dei tipi

L’aspetto di Scala che maggiormente mi affascina è quello di saper
declinare i costrutti più efficaci della programmazione all’interno di
un linguaggio tipizzato staticamente. Si rimprovera spesso ai più
diffusi linguaggi a tipizzazione statica, in particolare C++, Java e
C#, di essere prolissi, percorsi da un fortissimo “rumore sintattico”.
Scala, prendendo esempio da linguaggi quali Haskell e ML, cerca di
sciogliere questo nodo introducendo un meccanismo di href="http://en.wikipedia.org/wiki/Type_inference">inferenza dei
tipi molto sofisticato. Per garantire la corretta tipizzazione non
è necessario imboccare il compilatore, ma è il
compilatore stesso a dedurre, nella maggior parte dei casi, le
informazioni necessarie dal nostro codice. Per un esempio di tale
meccanismo, si noti che nel programma per il progetto Eulero appena
visto i tipi delle funzioni anonime non vengono dichiarati, ma dedotti
dal compilatore. Scala dimostra anche così che espressività,
concisione e tipizzazione statica non sono concetti antitetici.

Pattern matching

Il pattern matching è senza dubbio l’aspetto più controverso del
linguaggio. Da un lato è sicuramente uno strumento potentissimo,
sfruttabile ad esempio nella definizione di funzioni per induzione o
per manipolare di documenti XML, i quali, per inciso, sono tipi
“primitivi” del linguaggio:

val magazzino01 = 
  <magazzino>
    <articolo codice="nxt01" decrizione="Robot Lego NXT" esistenza="1" />  
    <articolo codice="nabas02" decrizione="Nabagstag Tag" esistenza="4" />  
  </magazzino>;

def add(magazzinoDaAggiornare: Node, nuovoArticolo:Node) = 
  magazzinoDaAggiornare match {
    case <magazzino>{ articoli @ _* }</magazzino> => 
            <magazzino>{ articoli }{ nuovoArticolo }</magazzino>
  }

val magazzinoAggiornato = 
  add(magazzino01, 
      <articolo codice="chu01" descrizione="Radio Chumby" esistenza="2" />)

D’altra parte, la stessa definizione che di esso ne dà Odersky
lascia alquanto perplessi: Il pattern matching è una
generalizzazione del costrutto switch del C o di Java applicato alla
gerarchia delle classi.
In altre parole un super
switch effettuabile sul risultato di una
instanceof. Qualcosa che sembrerebbe poco in linea con i
principi della programmazione ad oggetti (che, ad ogni modo, non è un
dogma di fede). Prima di rimandarvi a questo href="http://www.artima.com/forums/flat.jsp?forum=106&thread=166742">
interessante articolo dello stesso Odersky sull’argomento, vediamo
un esempio di pattern matching su una gerarchia di classi. Il pattern
matching su gerarchie di classi si sposa in modo particolare con i
tipi di dati algebrici, quali liste alberi e stack. Supponiamo di
definire con le seguenti tre classi un albero binario di interi:

abstract class Tree
case object Empty extends Tree
case class Binary(element: int, left: Tree, right: Tree) extends Tree

Il modificatore case indica che sulla struttura della
classe in via di definizione sarà possibile effettuare il pattern
matching. Tramite pattern matching portremo allora definire un metodo
per trovare la somma degli interi contenuti nell’albero:

def sum(t: Tree): int = t match {
  case Empty => 0
  case Binary(element, left, right) => element + sum(left) + sum(right)
}

Tipi strutturali e conversioni implicite

Ecco una delle caratteristiche pù interessanti del linguaggio.
Supponiamo di avere un metodo che cancella i record di una tabella di
database:

def deleteAllRows(statement: java.sql.Statement) = 
  statement.execute("DELETE FROM MYTABLE")

Dovendo scrivere un test per questo metodo, si potrebbe pensare di
utilzzare come parametro non un vero oggetto JDBC, ma un mock object,
per poi verificare la correttezza dell’SQL che si manda in esecuzione.
A tale scopo, in Java, dovrei creare un mock che implementa i circa
quaranta metodi dell’interfaccia java.sql.Statement,
quando sarei interessato al solo execute. È vero che
potrei utilizzare una classe proxy, ma la cosa sarebbe comunque
macchinosa. Se anche volessi passare a deleteAllRows
un’interfaccia contenente il solo metodo di mio interesse, essa non
potrebbe venire implementata da java.sql.Statementche
risiede una libreria. Mi occorrebbe una classe all’interno della quale
incorporare il vero statement. Scala ha una soluzione più elegante al
problema. Possiamo riscrivere deleteAllRows così:

def deleteAllRows(statement: {def execute(sql: String): Boolean}) = 
  statement.execute("DELETE FROM MYTABLE")

Il che significa che il parametro statement deve avere
una struttura che comprenda il metodo execute. Si noti
che questa dichiarazione di tipo, oltre a limitare un uso scorretto
del metodo, costituisce anche una forma di documentazione per chi lo
dovrà utilizzare. La documentazione automatica del codice è uno dei
punti di forza dei linguaggi a tipizzazione statica rispetto a quelli
tipizzati a tempo di esecuzione. Il codice di test potrebbe essere
allora di questo tipo:

def testDeleteAllRows() {
  val mockStatement = new {
    def execute(sql: String) = {
      println(sql)   //Oppure qualsiasi cosa per testare l'oggetto
      true          //Valore di ritorno di execute
    }
  }

  deleteAllRows(mockStatement)
}

Rimanendo nel nostro esempio, supponiamo ora di aver trovato una
libreria che consenta l’esecuzione di chiamate SQL su un database
remoto accessibile non via JDBC, ma tramite web services. Ecco come
potrebbe essere definito uno statement remoto:

class RemoteStatement {
  def remoteExecute(sql: String) = {
    println(sql + " executed remotely :-)")
    true
  }
}

Se l’autore della libreria avesse utilizzato la stessa nomenclatura
dell’interfaccia java.sql.Statement non avremmo dovuto
fare nulla. Siccome il metodo che ci interessa è stato invece chiamato
remoteExecute, dobbiamo modificare il nostro codice:

def deleteAllRows[T <% {def execute(sql: String): Boolean}](statement: T) = 
  statement.execute("DELETE FROM MYTABLE")

La notazione tra parentesi quadre indica, analogamente ai generics
di Java, che il metodo è parametrico nel tipo T. Su tale tipo si
introduce però un vincolo, detto view bound e denotato da
<%, che richiede che esso sia convertibile in un tipo
contenente il metodo execute. Si noti che, dopo questa
modifica, tutti i client del metodo, sia quelli che utilizzano
java.sql.Statement che quelli che ricorrono al mock
continuano a funzionare. Per utilizzare la libreria occorre creare una
funzione che converta RemoteStatement nel tipo
voluto:

implicit def normalizeStatement(remote: RemoteStatement) = new {
  def execute(sql: String) = remote.remoteExecute(sql)
}

Siamo quindi ora in grado di effettuare la chiamata al nostro metodo
utilizzando la nuova classe:

deleteAllRows(new RemoteStatement)

Per approfondire l’argomento consiglio di dare un’occhiata al href="http://scala-blogs.org/2007/12/scala-statically-typed-dynamic-language.html">post
di David Pollak che, tralasciando le note eccessivamente
polemiche, è senza dubbio illuminante.

Gli “Attori” della programmazione concorrente

L’importanza della programmazione concorrente nell’ultimo periodo
e’ aumentata, da un lato per l’uscita dei processori multi-core,
dall’altro per l’affermarsi di paradigmi di elaborazione distribuita
quali i web-services ed il grid computing.

La concorrenza in Java è gestita generalmente tramite threads
con memoria condivisa sottoposta a lock. Questo metodo da un lato ha
problemi di prestazioni, in quanto l’inizializzazione e il
passaggio di contesto fra thread sono operazioni dispendiose per
elaborazione e memoria; d’altro canto è difficilmente
estendibile ad un ambienti distribuiti. Inoltre questo stile di
programmazione risulta difficile da comprendere e testare.

Scala ha importato dal linguaggio Erlang la tecnica di gestione
della concorrenza basata su Actor, processi concorrenti
che comunicano inviandosi messaggi sia in modo sincrono che asincrono.
Ciò è vantaggioso sia perché gli Actor possono essere
implementati sfruttando un meccanismo di thread-pooling che ne aumenta
la scalabilità, sia perché il modello potrebbe essere facilmente
esteso all’elaborazione distribuita. Il codice risulta per giunta più
lineare, deterministico e meno soggetto ad “effetti collaterali”.
Quale esempio, vediamo un semplice programma che effettua la ricerca
parallela di un file.

import scala.actors.Actor
import java.io.File

//I 2 tipi di messaggi che i processi si inviano
case class Found(location: String)
case object Finished

case class ParallelSearch(toFind: String) extends Actor {   
  def act() = {  //Analogo al metodo run() di Runnable in Java
    Searcher(toFind, "c:\\", this).start  //Avvio il primo processo di ricerca...
    Searcher(toFind, "e:\\", this).start  //...ed il secondo
    var activeWorkers = 2
    while(activeWorkers > 0) {
      receive {  //Pattern matching sul tipo di messaggio ricevuto
        case Found(location) => println("Found " + location)
        case Finished  => activeWorkers -= 1
      }
    }
    println("Search completed")
  }  
}

case class Searcher (what: String, where: String, search: ParallelSearch) extends Actor {
  def act() = {
    visitAllDirsAndFiles(new File(where))
    search ! Finished   //Segnalo alla ricerca di avere terminato
  }

  def visitAllDirsAndFiles(fileSystemEntry: File): Unit = {
    if (fileSystemEntry.getName().equalsIgnoreCase(what))
      search ! Found(fileSystemEntry.getAbsolutePath()) //Invio il percorso trovato
    if (fileSystemEntry.isDirectory()) {
      val children = fileSystemEntry.list()
      for (child <- children) {
        visitAllDirsAndFiles(new File(fileSystemEntry.getAbsolutePath(), child))
      }
    }
  }
}

object parallelSearchRunner extends Application {
  ParallelSearch("parallelSearchRunner.scala").start
}

Per inviare ad un Actor un messaggio asincrono,
costituito da un oggetto, si utilizza il metodo actorObject !
msgObject
, mentre all’interno di un Actor posso
ricevere messaggi utilizzando il metodo receive. Si noti
che entrambi non sono costrutti insiti nel linguaggio, bensì due
metodi della classe di libreria Actor. Questa scelta
comporta alcuni benefici. In primo luogo il cuore del linguaggio può
mantenersi snello ed adattarsi agevolmente al sistema di threading
della virtual machine che lo ospita. Secondariamente risulta possibile
estendere la libreria con implementazioni differenti da quella di
base, che, ad esempio, realizzino la comunicazione con attori remoti
utilizzando diversi protocolli di trasporto.

La nostra banale ricerca parallela potrebbe trasformarsi
facilmente, disponendo di un’opportuna libreria per la comunicazione
con processi remoti, in un’applicazione di ricerca distribuita. Un
supporto agli Actor distribuiti è presente nella libreria base di
Scala e href="http://jonasboner.com/2008/01/25/clustering-scala-actors-with-terracotta/">parecchio
lavoro si sta svolgendo per l’integrazione con href="http://www.terracotta.org">Terracotta. Non sono molti i
linguaggi che si propongono come alternative a Java a possedere un
modello di programmazione concorrente così evoluto.

Conclusioni

Vi sarebbero numerose altre caratteristiche interessanti del
linguaggio, ma è giunto il momento di qualche riflessione. Scala è
forse il migliore dei linguaggi possibili? Certamente molti aspetti di
Scala non sono ottimali. Ho già citato i dubbi sul pattern matching.
La grande ricchezza sintattica inoltre, se male utilizzata, può
portare a del codice intricato e difficilmente leggibile. Ad esempio
qualche tempo fa è nata una href="http://creativekarma.com/ee.php/weblog/comments/my_verdict_on_the_scala_language/">polemica
sulla leggibilità del seguente frammento:

def sum(l: List[int]): int = (0/:l){_+_}

È il frutto di una manata casuale sulla tastiera, o il metodo per
sommare una lista di interi? Alcune sottigliezze, poi, come la
distinzione tra funzioni senza argomenti e funzioni con zero
argomenti, pur ineccepibili da un punto di vista teorico, possono
apparire barocchismi a chi proviene dal mondo, forse un po’ grezzo, ma
concreto di Java. La gerarchia dei tipi, con la distinzione tra
Any, Anyref ed Anyval e la
conseguente suddivisione tra Null e Nothing,
probabilmente ha subito la cattiva influenza di Java, che distingue
tra valori primitivi ed oggetti.

Il compilatore non è esente da bachi ed il plug-in per Eclipse, pur
utile, è sicuramente da raffinare; ma questi, si sa, sono peccati di
gioventù ai quali sicuramente si porrà rimedio. Vanno però considerati
i punti di forza della creatura di Odersky e soci. In primo luogo
Scala è pienamente integrato nell’ambiente di esecuzione Java. Ciò
consente il suo utilizzo in una grande quantità di ambienti eterogenei
(ad esempio i sistemi Mainframe e iSeries IBM) oltre a mettere
immediatamente a disposizione l’enorme numero di librerie e framework
esistenti per il linguaggio di Sun. Si noti poi che le prestazioni di
Scala sono paragonabili a quelle di Java, cosa non sempre vera per
molti suoi concorrenti. Inoltre il linguaggio, pur potentissimo, è a
tipizzazione statica. Questo può facilitarne l’adozione da parte di
chi, come me, non ha una grande affinità con i linguaggi dinamici.
Anche la sua sintassi di base, simile a quella di Java, può
contribuire alla sua diffusione. Secondo la mia modestissima
opinione, quindi, Scala non è un linguaggio perfetto, ma, al momento,
sembra comunque uno dei migliori candidati alla successione di Java
nel medio periodo.

Comments

  1. Premetto di non amare particolarmente Java, quindi non commento il lato prettamente tecnico dell’articolo, ma non posso non congratularmi con voi per la tipologia delle notizie, l’impostazione dell’esposizione e l’elevato livello degli argomenti trattati: veramente bravi!
    Sinceramente, penso che questo blog sia veramente uno dei migliori attualmente presenti sulla scena italiana. Mai banale, molto interessante e dettagliato, ma non per questo complicato da leggere.
    Bravi.

  2. Michele Simionato says:

    Io aspetto un articoletto sui pro e contro del
    pattern matching!

  3. A sentire un mio collega che usa erlang il pattern matching è il Sacro Graal ;-)

  4. Franco, ottima panoramica del linguaggio e benvenuto nel team. :)

  5. io voglio sapere la differenza tra funzioni con zero argomenti e funzioni senza argomenti! Ormai ho un tarlo nel cervello, ci scappa un’articoletto ? :)

    Tra l’altro, esiste qualcuno che sa se e quali differenze ci sono tra il pattern matching in Scala e quello in F# ?

    Ah e dimenticavo: bel’articolo!

  6. lloyd27 says:

    un altro linguaggio java-based carino è Groovy, con qualche infusione di python.. ma anche questo scala (che onestamente non conoscevo) non sembra da buttare..

  7. Considerando il livello tecnico medio dei programmatori Java con cui ho avuto a che fare sinceramente li vedrei in difficoltà a gestire un linguaggio così barocco. Ne vedo la diffusione difficile, sicuramente sarà precursore delle future evoluzioni di Java. Ottimo articolo.

  8. def sum(l: List[int]): int = (0/:l){_+_}

    O_o

    è la più brutta foldr / reduce che abbia mai visto

    Boh, magari sono io, ma più vedo questo linguaggio meno m’attira (ottimo articolo, comunque)

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.