Overriding

Si consideri l’esempio della classe astratta IntSet, con le due sottoclassi concrete che ne realizzano un’implementazione (basata su un BST):

abstract class IntSet {
	def add(x: Int): IntSet
	def contains(x: Int): Boolean
}

class Empty extends IntSet {
	def contains(x: Int): Boolean = false
	def add(x: Int): IntSet = new NonEmpty(x, new Empty(), new Empty())
}

class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {
	def contains(x: Int): Boolean =
		if (x < elem) left contains x
		else if (x > elem) right contains x
		else true

	def add(x: Int): IntSet =
		if (x < elem) new NonEmpty(elem, left add x, right)
		else if (x > elem) new NonEmpty(elem, left, right add x)
		else this
}

Le definizioni di contains e add delle sottoclassi Empty e NonEmpty implementano i metodi astratti dichiarati nella superclasse IntSet. Se invece si implementa un metodo che è già defnito in una classe base, allora si parla di overriding, sovrascrittura. Come già detto, Scala richiede di dichiarare esplicitamente la sovrascrittura dei metodi usando il modificatore override. Si può mettere override anche quando si implementa un metodo astratto della superclasse, ma non è obbligatorio.

Esempio: toString

Definzione di singleton object

Siccome la classe Empty non ha parametri e non fornisce operazioni di modifica, tutte le sue istanze sono identiche, dunque non vi è la necessità di avere istanze diverse. Allora, si potrebbero risparmiare il tempo e la memoria necessari a costruire le istanze di Empty riutilizzando sempre un’unica istanza, ottenendo così alberi con una struttura del genere:

Untitled

In Java, si può “forzare” la creazione di un’unica istanza di una classe usando un design pattern: il singleton. Scala internalizza tale pattern nel linguaggio tramite le object definition: usando la parola riservata object invece di class si definisce singleton object, cioè un oggetto che è l’unica istanza della sua classe ed è associato a un nome uguale a quello della classe.

Ad esempio, la definizione

object Empty extends IntSet {
	def contains(x: Int): Boolean = false

	def add(x: Int): IntSet = new NonEmpty(x, Empty, Empty)

	override def toString: String = "."
}

crea un singleton object di nome Empty. Più nel dettaglio, il nome Empty fa riferimento all’unico oggetto di tipo Empty, che viene automaticamente costruito dal compilatore, mentre non è possibile costruire esplicitamente istanze di Empty (usando l’operatore new).

Un singleton object è un valore, che nel modello di sostituzione viene rappresentato dal suo nome. Di conseguenza, tale nome (ad esempio Empty) viene valutato in se stesso, è “già valutato”.

Applicazioni stand-alone

Quando si realizza un’applicazione in Java, il punto di ingresso (da cui inizia l’esecuzione del codice) è il metodo statico main. Anche se i metodi statici sono scritti all’interno di una classe, essi non sono associati ad alcuna istanza, dunque la creazione del primo “vero” oggetto di un programma orientato agli oggetti spetta al programmatore.

Invece, Scala adotta un approccio orientato agli oggetti più puro: il main è un metodo “tradizionale”, non statico ma appartenente a un singleton object che viene istanziato automaticamente dal compilatore (come tutti i singleton object). Quello che invece rimane uguale a Java è il tipo del metodo main: esso deve avere un singolo parametro di tipo array di stringhe (gli argomenti passati al programma dalla riga di comando):

def main(args: Array[String]) = /* ... */

Ad esempio, il punto d’ingresso di un’applicazione Scala potrbbe essere definito da un file con i seguenti contenuti:

package lib.intset

object Main {
	def main(args: Array[String]) = {
		println("Some IntSets")
		val set1 = Empty.add(1)
		println(set1)
		// ...
	}
}

Una volta compilata l’applicazione, per eseguirla da riga di comando è necessario usare il comando scala (analogo a java), passando come argomento il nome completo dell’oggetto che contiene il metodo main: