Creare una classe in Scala

Creare una classe in Scala

In questo post vedremo come creare una classe in Scala: avremo un approccio “morbido”, scriveremo una classe in Scala come se stessimo programmando in Java e via via la traghetteremo verso una sintassi da Scalista (?) navigato. In pochi semplici passi scopriremo come Scala ci permette di liberarci di molto codice, pur mantenendo la stessa espressività. Ci serve qualcosa di semplice e per questo esempio propongo di modellare un hard disk, un tipo di dato con due parametri: la capacità iniziale e lo spazio occupato. Partiamo!

Definiamo classe e member variable

Cominciamo dichiarando la classe e i due dati che vogliamo memorizzare ed esaminiamo la sintassi riga per riga.

package it.cosenonjaviste;

class HardDisk  {

  private var _size : Int = 0;
  private var _usedSpace : Int = 0;
  
}

L’indicazione del package è uguale a Java, la definizione della classe invece cambia un po’. Non c’è scritto public perché di default in Scala tutto è pubblico. La ragione è semplice: si dichiarano più spesso classi pubbliche che private. Se invece desiderate mettere qualcosa di privato basta esplicitarlo.

Passiamo alle member variable, esamineremo in dettaglio _size, la prima, per la seconda valgono considerazioni analoghe. É privata ed è var. Questa è una differenza rispetto a Java. In Scala una variabile deve essere obbligatoriamente dichiarata val o var. Nel primo caso il valore una volta assegnato non è più modificabile, quindi corrisponde al final di Java; nel secondo caso invece il valore può essere modificato successivamente. Altra differenza che salta all’occhio è che il tipo della variabile è scritto dopo il nome.

Aggiungiamo getter e setter

La classe HardDisk ha bisogno di setter e getter per poterne modificare i valori interni. Qui Scala differisce rispetto alla convenzione Java get/set, ma, appunto si tratta solo di una convenzione diversa.

  def size: Int = {
    _size;
  }

  def size_=(newSize: Int) : Unit = {
    _size = newSize;
  }

  def usedSpace: Int = {
    _usedSpace
  };

  def usedSpace_=(newUsedSpace: Int) : Unit = {
    _usedSpace = newUsedSpace;
  }

Ogni metodo è dichiarato con la keyword def, il nome, la lista di parametri eventualmente vuota e il tipo di ritorno del metodo. Il metodo alla linea 1 è il getter di _size e ritorna un tipo Int. La firma del metodo è seguita da un segno di uguale e il corpo del metodo tra parentesi graffe. Detto questo, il metodo size acquista significato, manca solo una cosa rispetto a Java: la keyword return. Per Scala, l’ultima espressione è quella che viene ritornata dal metodo e che deve coincidere con il tipo di ritorno dichiarato nella firma.

Passiamo al setter size_= che per convenzione è “legato” al getter tramite i simboli di underscore e uguale. L’unica e ultima riga del metodo assegna il parametro alla member variable _size, quindi il metodo ritornerà il tipo di _size = newSize;. Ma di che tipo è questa istruzione? E’ un assegnamento e per Scala l’unico tipo ammissibile è Unit che possiamo far corrispondere grossomodo a void. Questo spiega il tipo di ritorno indicato nella firma.

Queste considerazioni valgono per gli altri due metodi che quindi non spiegheremo, piuttosto andiamo a scrivere del codice client dove creiamo un’istanza di HardDisk.

var hd = new HardDisk();
hd.size_=(250)
hd.usedSpace_=(10);
println("Il disco ha " + hd.size +  " GB");

Anche qui non stiamo scrivendo puro codice Scala, ma per il momento ci basta per capire che le cose così come le abbiamo scritte funzionano.

Costruttore e toString

La nostra classe andrebbe già bene così com’è ma non possiamo negarle un costruttore e un metodo toString utile per debuggare.

  def this(startingSize: Int, startingUsedSpace: Int) = {
    this();
    _size = startingSize;
    _usedSpace = startingUsedSpace;
  }


  override def toString: String = {
    "HardDisk[Size " + _size + ", Used space " + _usedSpace + "]";
  }

Il primo metodo è il costruttore, quindi inizia per def; ma mentre in Java ha lo stesso nome della classe, in Scala al posto del nome della classe si usa la keyword this. Sui costruttori torneremo in un prossimo post, ma qui faccio notare che stiamo chiamando anche this() che è il costruttore “principale” – e senza parametri – della classe. In Scala ogni costruttore deve chiamare il costruttore principale o un altro costruttore.

Il metodo toString invece è una semplice concatenazione di oggetti ma, dato che è già presente nell’oggetto di base da cui derivano implicitamente tutti gli altri oggetti, siamo obbligati a specificare la keyword override.

Bene, ora la nostra classe Ja.. Scala è completa e funzionante. Siamo pronti a scriverla meglio, prima però, per completezza, la riporto tutta intera. A parte le differenze sintattiche, non abbiamo dato una definizione tanto diversa da quella Java. Ora, però, Scala entra veramente in azione e passo dopo passo toglieremo le cose “inutili” per arrivare ad una dichiarazione più standard (e più corta).

package it.cosenonjaviste;

class HardDisk {

  private var _size: Int = 0;
  private var _usedSpace: Int = 0;

  def size: Int = {
    _size;
  }

  def size_(newSize: Int) :Unit = {
    _size = newSize;
  }

  def usedSpace: Int = {
    _usedSpace
  };

  def usedSpace_(newUsedSpace: Int) :Unit = {
    _usedSpace = newUsedSpace;
  }

  def this(startingSize: Int, startingUsedSpace: Int) = {
    this();
    _size = startingSize;
    _usedSpace = startingUsedSpace;
  }


  override def toString: String = {
	  "HardDisk[Size " + _size + ", Used space " + _usedSpace + "]";
  }

}

Togliamo quello che non serve: le cose spicciole

Come promesso, vedremo come dichiarare una classe in Scala secondo una sintassi più tipica. É venuto quindi il momento di fare pulizia: partiamo da cose spicciole, ma gradite. Innanzitutto Scala non ha bisogno dei punti e virgola quando c’è una sola istruzione per riga e se c’è una sola istruzione per riga, il corpo di un metodo non ha bisogno delle parentesi graffe. Anche le parentesi tonde sono di troppo se non ci sono parametri. Riscriviamo la nostra classe secondo queste regole.

package it.cosenonjaviste

class HardDisk {

  private var _size: Int = 0
  private var _usedSpace: Int = 0

  def size: Int = _size
  def size_(newSize: Int): Unit = _size = newSize

  def usedSpace: Int = _usedSpace
  def usedSpace_(newUsedSpace: Int): Unit = _usedSpace = newUsedSpace

  def this(startingSize: Int, startingUsedSpace: Int) = {
    this
    _size = startingSize
    _usedSpace = startingUsedSpace
  }


  override def toString: String = "HardDisk[Size " + _size + ", Used space " + _usedSpace + "]"

}

Togliamo i tipi

Scala è in grado di inferire i tipi di ritorno dei metodi e delle variabili. Nel nostro caso possiamo eliminare tutti i tipi dei getter e setter perché il tipo di ritorno di un metodo è quello del valore ritornato dall’ultima riga. Possiamo togliere i tipi anche dalle member variable: in fin dei conti vengono inizializzate con un valore che ha già un tipo.

package it.cosenonjaviste

class HardDisk {

  private var _size = 0
  private var _usedSpace = 0

  def size = _size
  def size_(newSize: Int) = _size = newSize

  def usedSpace = _usedSpace
  def usedSpace_(newUsedSpace: Int) = _usedSpace = newUsedSpace

  def this(startingSize: Int, startingUsedSpace: Int) = {
    this
    _size = startingSize
    _usedSpace = startingUsedSpace
  }

  override def toString = "HardDisk[Size " + _size + ", Used space " + _usedSpace + "]"

}

Togliamo ancora..

Fin qui abbiamo visto cose carine, le nostre dita ringraziano, ma siamo comunque davanti ad una sintassi “Java-like”. Tutto qui? No, possiamo evitare anche i getter e i setter, Scala li genererà per noi. Riscriviamo la classe, che, se avete notato, ad ogni passaggio diventa più corta.

package it.cosenonjaviste

class HardDisk {

  var size = 0
  var usedSpace = 0

  def this(startingSize: Int, startingUsedSpace: Int) = {
    this
    size = startingSize
    usedSpace = startingUsedSpace
  }

  override def toString = "HardDisk[Size " + size + ", Used space " + usedSpace + "]"

}

Qui sembra che Scala imbrogli: pare che per eliminare i setter e i getter basti rendere pubbliche le member variable, questo si può fare anche in Java! In realtà, con questa dichiarazione, Scala ha generato per noi le member variable private size e usedSpace e i corrispondenti setter e getter. Per verificare che questo corrisponde al vero, dopo aver compilato, possiamo eseguire sul file Scala.class il comando javap che decompila e mostra i membri di una classe.

someLaptop: giampaolo$ javap -private HardDisk
Warning: Binary file HardDisk contains it.cosenonjaviste.HardDisk
Compiled from "HardDisk.scala"
public class it.cosenonjaviste.HardDisk {
  private int size;
  private int usedSpace;
  public int size();
  public void size_$eq(int);
  public int usedSpace();
  public void usedSpace_$eq(int);
  public java.lang.String toString();
  public it.cosenonjaviste.HardDisk();
  public it.cosenonjaviste.HardDisk(int, int);
}

Come si vede dall’output di javap, Scala ha generato per noi tutto quello che ci serve per lavorare. Prima di proseguire però fermiamoci un attimo. Abbiamo eseguito javap su di un file class, il “binario” che siamo abituati a vedere quando compiliamo con Java, però stavamo lavorando in Scala. Qual è la spiegazione? Al pari del compilatore Java, quello Scala genera del bytecode, il linguaggio che la JVM è in grado di eseguire e che viene memorizzato in file con estensione class. Per la virtual machine, il codice compilato da una sorgente Java è indistinguibile da quello compilato partendo da Scala per cui possiamo usare gli stessi strumenti.

Togliamo tutto!

Siamo pronti ora all’ultimo passaggio, quello dove elimineremo tutto il resto! Torneremo in futuro sui costruttori, per il momento ci basta sapere che possiamo sostituire il costruttore principale con il nostro. Per far questo andiamo ad usare una notazione diversa da quella vista finora. Metteremo le member variable direttamente nel nome della classe; di fatto stiamo cambiando il costruttore principale.

package it.cosenonjaviste

class HardDisk(var size: Int, var usedSpace: Int) {

  override def toString = "HardDisk[Size " + size + ", Used space " + usedSpace + "]"
}

A meno del costruttore di default che è cambiato, questa dichiarazione equivale quella dell’esempio precedente. I più curiosi possono riprovare javap e verificare. Faccio notare piuttosto che Scala fonde il concetto di costruttore con quello di dichiarazione di classe. Tutte le righe scritte dentro le parentesi graffe vengono eseguite come se si trattasse di metodo. Quindi anche questa dichiarazione è valida:

package it.cosenonjaviste

class HardDisk(var size: Int, var usedSpace: Int) {
	println("sto preparando il tuo nuovo Hard Disk")

	override def toString = "HardDisk[Size " + size + ", Used space " + usedSpace + "]"
}

Per ogni nuova istanza di HardDisk non solo creeremo la classe ma anche stamperemo in console.

Un’altra cosa..

No, non è possibile restringere ancora la dichiarazione della classe :-), possiamo però migliorare la leggibilità del metodo toString. Scala dispone dell’interpolazione di stringa, il che vuol dire che posso usare dei “segnaposto” all’interno della stringa e questi verranno sostituiti con il valore delle variabili indicate dai segnaposto stessi. É più facile da fare vedere che da spiegare:

package it.cosenonjaviste

class HardDisk(var size: Int, var usedSpace: Int) {

  override def toString = s"HardDisk[Size $size, Used space $usedSpace]"
}

Come avrete notato, per abilitare l’interpolazione è necessario anteporre il carattere “s” alla stringa, poi basta riportare precedute dal simbolo dollaro le variabili che si vogliono includere nella stringa.

E il codice client?

Anche il codice che istanzia HardDisk può essere ripulito e scritto nella forma tipica di Scala.

var hd = new HardDisk(250, 20)
hd.usedSpace = 50

Come prima abbiamo tolto i punti e virgola e non stiamo più chiamando i setter in maniera esplicita. Anche se da Javisti l’impressione che abbiamo è quella di accedere ad una member variable pubblica, in realtà, grazie alle convenzioni su forma di setter e getter dette prima, stiamo accedendo alla variabile d’istanza privata passando attraverso il suo setter pubblico.

Conclusioni

Abbiamo visto come passare Scala ci semplifica la vita quando si tratta di definire una classe. Tutto quello che può essere risparmiato al programmatore, viene omesso secondo il principio “convention over configuration”. Giusto per dare un’idea numerica, la classe di partenza usava 404 caratteri, quella di arrivo 124. Scala ha moltissime altre caratteristiche interessanti che vedremo in futuro. Sicuramente la semplicità con cui si può creare una classe è un ottimo punto di partenza per iniziare ad apprezzare questo linguaggio di programmazione.

Giampaolo Trapasso

Sono laureato in Informatica e attualmente lavoro come Software Engineer in Radicalbit. Mi diverto a programmare usando Java e Scala, Akka, RxJava e Cassandra. Qui mio modesto contributo su StackOverflow e il mio account su GitHub