• Non ci sono risultati.

Capitolo 1 Introduzione ai linguaggi ad oggetti 1.1 Paradigmi di programmazione

N/A
N/A
Protected

Academic year: 2021

Condividi "Capitolo 1 Introduzione ai linguaggi ad oggetti 1.1 Paradigmi di programmazione"

Copied!
13
0
0

Testo completo

(1)

Capitolo 1

Introduzione ai linguaggi ad oggetti

1.1 Paradigmi di programmazione

Un paradigma di programmazione non è altro che un particolare stile di programmazione[28][31]. Per un linguaggio supportare un particolare paradigma non significa solo adottare quel paradigma, ma soprattutto supportare attivamente implementazioni basate su questo ultimo. Ciò significa che il linguaggio deve presentare i costrutti per rendere agevole lo sviluppo secondo le “regole” del paradigma adottato.

I principali paradigmi di programmazione includono:

Funzionale: Trova le sue origini storiche nel lambda-calcolo, il linguaggio

introdotto dal matematico Alonzo Church come formalismo universale (secondo appunto la nota Tesi di Church-Turing). La parola funzionale si deve al fatto che ogni simbolo di operazione che compare in una espressione corrisponde ad una funzione, che in tale paradigma costituisce un valore first-class. Nei linguaggi funzionali esistono dei meccanismi per cui è possibile rinunciare ai concetti di comando e di effetto. Il risultato è quello di avere dei programmi che godono della seguente proprietà, nota come “sostituibilità di uguali con uguali” o meglio come trasparenza referenziale: in una espressione F si possono sostituire le occorrenze di una sottoespressione e con il suo valore v senza che cambi il valore di F. Infine, i linguaggi funzionali, per dirsi veramente tali, devono prevedere le funzioni di ordine superiore: accettare funzioni come argomento, restituire funzioni come risultato e incorporarle in strutture dati. Queste possibilità consentono spesso di

(2)

Procedurale: E’ l’approccio tipico di linguaggi come Pascal e C. Alla base di

questo paradigma ci fu un tentativo di portare i linguaggi ad un livello più alto dell’allora usuale assembler. L’enfasi è qui posta su soluzioni algoritmiche e, appunto, procedure che operano su strutture dati. Le implementazioni realizzate in tali linguaggi risultarono subito estremamente efficienti, ma i problemi per i programmatori non tardarono ad arrivare. Prima fra tutti l’integrazione fra le varie componenti.

Modulare: Linguaggi come Ada e Modula-2 sfruttano la modularità. In questi

linguaggi un modulo nasconde le sue strutture dati agli altri moduli che lo usano. L’accesso avviene tramite le interfacce definite. Queste interfacce sono rese pubbliche, così da poter permettere un corretto utilizzo.

Object-Oriented: Tra tutti i paradigmi è il più recente. Può essere visto, per vie

generiche, come un’estensione dell’approccio modulare. Infatti non solo presenta moduli espliciti (gli oggetti appunto), ma inoltre a questi viene consentito di ereditare delle caratteristiche da altri. Viene a questo punto da chiedersi: perché un altro paradigma?

1.2 Perché i linguaggi ad oggetti?

L’approccio Object-Oriented[1][2][11] alla programmazione è basato su metodologie che poggiano su alcune caratteristiche comuni poi a tutti i linguaggi OO: rappresentazione multipla, ereditarietà, “incapsulamento”, sottotipi e open recursion. Vediamo brevemente cosa si intende per ciascuna di esse.

La rappresentazione multipla sta ad indicare il fatto che due oggetti possono riferirsi allo stesso insieme di operazioni pur avendo due rappresentazioni completamente diverse. Ciò significa che quando una operazione (metodo) verrà invocata su di un oggetto, sarà l’oggetto stesso a determinare il codice che dovrà essere eseguito (dynamic dispatch). Con ereditarietà si intende la possibilità da parte degli oggetti di condividere parte delle loro interfacce e di conseguenza alcuni comportamenti, che

(3)

verranno così implementati una volta per tutti. L’incapsulamento o trasparenza sta ad indicare il fatto che la rappresentazione interna di un oggetto è in genere nascosta all’esterno della sua definizione: solo i suoi metodi possono esplorarne e manipolarne i campi. La relazione di sottotipo mette in evidenza il fatto che se un oggetto soddisfa una certa interfaccia I, deve chiaramente soddisfare un’interfaccia J che fornisce meno operazioni di I, così da poter funzionare correttamente in un contesto che si aspetta un oggetto J e che dunque può solamente invocare operazioni di J. Con open recursion si intende l’abilità dei corpi dei metodi di invocare un altro metodo dello stesso oggetto per mezzo di una variabile speciale chiamata self (come in Sigma_X) o in alcune casi this (come in Java).

Il primo approccio alla programmazione ad oggetti fu originato con Simula, che inizialmente si dedicava alla costruzione di modelli. Le sue principali caratteristiche furono riportate in Smalltalk. Da quel momento in poi, l’approccio OO si diffuse in molti ambiti. La principali caratteristiche che hanno fatto la fortuna di tale modello sono state già viste all’inizio di questo paragrafo, ma in poche parole, possono essere riassunte nel seguente modo:

Ÿ L’analogia (fortissima) tra modelli software e modelli fisici. Ÿ L’elasticità dei modelli software.

Ÿ La riusabilità dei componenti dei modelli software.

Soprattutto questo ultimo aspetto si mostra molto interessante: Sigma_X, come vedremo in seguito, presenta tra le sue funzionalità l’ Extract (estrazione) che affronta l’aspetto dell’utilizzo di codice in contesti differenti da quello in cui è stato definito. L’uso di questo codice pone ovvi problemi quali: “chi è la variabile x utilizzata qui, in questo codice?” , “dove trovo il valore ad essa associato?” , “è sempre possibile fare un’estrazione e qual è il significato del codice estratto?” , “ha sempre la stessa funzione?”.

(4)

1.3 Il pioniere Smalltalk: oggetti e messaggi

L’intera programmazione in Smalltalk[20][21][23] si basa per lo più sull’invio di messaggi ad oggetti. Il risultato dell’invio di un messaggio ad un oggetto è un altro oggetto. Un approccio iniziale per la risoluzione dei problemi può essere quello di usare le tipologie di oggetti già definiti nella Smalltalk image (proprio come avviene con le API di Java). In breve, un programma scritto in Smalltalk non è altro che una sequenza di oggetti-messaggi.

Gli oggetti sono vere e proprie combinazioni di codice e dati. Il codice di un oggetto è “diviso in pezzi” chiamati metodi, e i dati sono contenuti in variabili (non tipate). I metodi sono qualcosa di simile alle subroutines, funzioni o procedure in altri linguaggi. Essi sono “blocchi” (il termine non è casuale) di codice identificati da un nome che possono essere chiamati (metodi di classe) o invocati (metodi di istanza) e restituire un valore alla fine della loro esecuzione. L’invocazione avviene tramite l’invio di un messaggio all’oggetto interessato. Un messaggio conterrà il “nome” del metodo e se richiesti i suoi parametri. Se l’oggetto a cui questo è stato inviato contiene il metodo si dice che esso comprende (understand) il messaggio, lo eseguirà e restituirà un risultato. L’oggetto che invia il messaggio è chiamato sender, quello che lo esegue receiver. Questa interazione tra oggetti, a prima vista, sembrerebbe implicare una sorta di parallelismo, con oggetti che inviano messaggi ad altri in parallelo. In realtà, chi invia un messaggio è bloccato finché non riceve la risposta. L’unica cosa consentita è che il receiver durante l’esecuzione del metodo richiesto possa inviare altri messaggi bloccandosi anch’esso in attesa della risposta. La send è dunque bloccante come la chiamata di funzione in un linguaggio non OO (anche se Smalltalk presenta una seppur “primitiva” implementazione dei thread). Notare come in essa avvenga un vero e proprio passaggio dei metodi come parametri: infatti, a livello implementativo, il nome del metodo assomiglia più ad un riferimento. Escluse eventuali (vedere message chaining) anomalie del dispatcher.

(5)

1.4 Il pioniere Smalltalk: definire e creare gli oggetti

Come osservato, programmare in un linguaggio OO come Smalltalk significa definire gli oggetti e specificare le loro interazioni. Ovviamente ogni oggetto non viene ideato singolarmente. I programmatori specificano “speciali” oggetti, conosciuti in Smalltalk (e non solo) come classi (oggetto). Queste funzionano un po’ come dei template. Il loro scopo è quello di rappresentare le funzionalità (metodi e variabili) di un insieme (collezione) di oggetti. Questi ultimi prendono il nome di istanze. A differenza dei template però, le classi oggetto di Smalltalk hanno anche il compito di creare appunto le istanze. Tale processo prende il nome di istanziazione. La relazione tra classi e istanze porta le prime a “combinare” due tipologie di metodi e dati. Quelli che sono propri della classe (metodi e variabili di classe), e quelli invece che andranno a far parte di ogni istanza (metodi e variabili di istanza). Le classi comprendono (understand) solo i metodi di classe, gli oggetti solo quelli di istanza. E’ una distinzione importante, frequentemente causa di confusione.

Ogni istanza di una particolare classe, ha un proprio insieme di variabili (di istanza) definite appunto in quella classe: tale insieme non è condiviso. La situazione è diversa per le variabili di classe che sono visibili e condivise fra tutte le istanze. Sebbene concettualmente verrebbe da pensare che ognuna di queste abbia una propria copia dei metodi di istanza definiti nella classe, in pratica sarebbe davvero inefficiente. Così in Smalltalk, le istanze condividono i metodi definiti nella loro classe.

In breve, ricapitolando:

Variabili di istanza: un insieme separato in ogni istanza.

Variabili di classe: un insieme condiviso fra la classe e le sue istanze.

Metodi di istanza: definiti nella classe, ma compresi solo dalle istanze.

(6)

1.5 Il pioniere Smalltalk: ereditarietà e gerarchie

Si è già detto come in Smalltalk gli oggetti non siano ideati singolarmente, ma come istanze di una data classe. Così come si è osservato che molte istanze sono simili ad altre, stessa cosa si può fare per le classi oggetto. Questa somiglianza porta ad un concetto chiamato ereditarietà. Infatti sfruttando l’ereditarietà, un programmatore può affermare che una nuova classe è simile ad una già esistente, tranne che in qualcosa. La nuova è chiamata sottoclasse, l’esistente superclasse. Si dice che la sottoclasse eredita dalla superclasse. Una classe può avere molte sottoclassi che ereditano da essa; ogni sottoclasse può ereditare solo da una superclasse (no ereditarietà multipla). Ogni classe può avere sia una sotto che una superclasse. Questo meccanismo porta alla definizione di una specie di “albero di famiglia” chiamato gerarchia.

Le istanze di una particolare classe comprende tutti i metodi definiti in essa insieme ai metodi definiti nella superclasse. Stessa cosa per le variabili di istanza. Normalmente è possibile creare istanze di ogni classe, e non soltanto, di quelle al livello più basso della gerarchia (foglie). Può succedere però che si voglia definire una qualche classe che funzioni come un protocollo per le sottoclassi: si parla allora di classi astratte. Chiameremo concrete invece le classi di cui è possibile creare delle istanze.

1.6 Il pioniere Smalltalk: ereditarietà, un esempio

Completiamo il discorso sull’ereditarietà esemplificando i concetti esposti con un esempio concreto[24].

Supponiamo di avere tre classi: Class1, Class2, Class3. Class1 è sottoclasse di

Object, Class2 è sottoclasse di Class1 e Class3 è sottoclasse di Class2. Quando

viene creata una istanza di Class3, essa conterrà tutte le variabili di istanza definite nelle classi da 1 a 3 più quelle di Object.

(7)

Avendo una istanza della Class3, possiamo inviare ad essa un messaggio con la richiesta dell’esecuzione di un qualche metodo. Ricordiamo che i corpi dei metodi si trovano nelle classi e non nelle istanze. Questo significa che dapprima il sistema lo cercherà nella classe della istanza (Class3 nel nostro caso). Se il metodo c’è, verrà eseguito e la ricerca si fermerà. Altrimenti essa continuerà nella superclasse immediata (Class2) e così via fino alla classe Object. Se il metodo richiesto non viene trovato, il processo di ricerca terminerà con l’esecuzione di doesNotUnderstand di Object.

1.7 Il pioniere Smalltalk: message chaining e yo-yo problem

Il processo in precedenza descritto unito al meccanismo conosciuto in Smalltalk come message chaining (catena di messaggi) pone un problema legato all’efficienza che va sotto il nome di yo-yo problem[22]. Tale problema si verifica perché la ricerca del metodo da eseguire parte dalla corrente classe di istanza, anche se esso è definito nella sua superclasse.

Supponiamo di trovarci di fronte alla chiamata Number asInteger asString, con Number, riferendoci all’esempio precedente, istanza di Class3. La ricerca del metodo inizia dalla classe di istanza (Class3), ma la sua definizione viene trovata in Class1 (asInteger). A questo punto, abbiamo la chiamata asString: si riparte da Class3: questa volta la definizione si trova in Object. Osservando lo schema qui in fondo capiamo il perché del nome yo-yo. Si può ben capire che l’esecuzione diventi via via più inefficiente man mano che le gerarchie si fanno più complesse.

(8)

1.8 Il pioniere Smalltalk: overriding e polimorfismo

L’ereditarietà in Smalltalk ha un significato additivo. Ogni classe eredita tutte le funzionalità della sua superclasse aggiungendone delle proprie. Ma cosa succede se una classe cerca di aggiungere con lo stesso nome, alcune funzionalità che sono già state ereditate?

Se una classe cerca di definire una nuova variabile con lo stesso nome di una già ereditata, il risultato è semplicemente un errore.

Invece, se cerca di ridefinire un metodo, il nuovo sostituisce l’ereditato nelle istanze di quella classe, e nelle sue sottoclassi. Questo processo prende il nome di overriding. Ovviamente il metodo originale non scompare. Più in generale, due classi differenti possono definire due metodi differenti con lo stesso nome. Questa capacità unita al meccanismo dell’overriding, fornisce alla programmazione OO uno strumento molto potente. Quando un messaggio è inviato ad un oggetto con la richiesta di un certo metodo, la sua esecuzione dipende dalla classe di cui questo ultimo è istanza. Tale caratteristica prende il nome di polimorfismo e permette a classi differenti di definire un proprio modo di fare la stessa cosa.

1.9 Riusabilità: scelte e meccanismi

E’ noto che i linguaggi Object-Oriented consentono il riuso di componenti software meglio dei moderni linguaggi procedurali. Un componente software è riusabile quando può essere facilmente usato in più di un contesto. Per esempio, un modulo può essere riusato importandolo in parecchi altri moduli, e un modulo generico può essere riusato istanziandolo con differenti parametri.

In ogni caso comunque il riuso “contempla” la sostituzione di un componente con un altro in un contesto. Nei linguaggi procedurali tradizionali, una sostituzione richiede un’esatta corrispondenza tra tipi e interfacce.

(9)

Nei linguaggi OO ci sono due tipi distinti di sostituzioni. Gli oggetti possono essere rimpiazzati da altri oggetti e i metodi possono essere rimpiazzati da altri metodi. E’ da notare che queste forme di replace non richiedono un’esatta corrispondenza di tipi o interfacce.

Vari meccanismi consentono di rimpiazzare oggetti. In generale, si può sostituire un oggetto con uno nuovo che ha almeno lo stesso insieme di attributi. Ogni attributo addizionale del nuovo oggetto rimane un attributo invisibile: essi sono conservati ma non direttamente accessibili. La sostituzione di metodi è chiamata overriding, mentre il riuso di metodi esistenti è chiamato ereditarietà. I nuovi oggetti, o i nuovi costruttori di oggetti, sono derivati dai vecchi “mixando” riuso e variazioni (ereditarietà e ovveride). Quando un metodo è sovrascritto da uno nuovo, questo ultimo deve essere conforme all’interfaccia del vecchio, ma con qualche possibilità di variazione. In particolare, il nuovo metodo deve essere più specifico del vecchio nell’interfaccia o nel comportamento, e deve usare gli attributi che sono disponibili solo sull’oggetto derivato. Un’altra inevitabile conseguenza di questo meccanismo di sostituzioni è la nozione di self. Usando self, un metodo può riferire i suoi oggetti ospiti, e dunque i loro metodi. Tramite i meccanismi di ereditarietà e override, i riferimenti ai metodi di un oggetto possono cambiare. Proprio per questo motivo self ha un significato dinamico. Questa nozione dinamica è un aspetto cruciale, perché consente ad un metodo di esibire un nuovo comportamento quando eredita un oggetto derivato. Senza tale nozione, il comportamento di un metodo è più rigido ed il riuso è limitato. Un’altra conseguenza di questi meccanismi è che i metodi sono strettamente legati agli oggetti. A causa di questa flessibilità, non possiamo conoscere staticamente l’oggetto legato dinamicamente alla variabile. D’altronde, non si possono conoscere staticamente quali metodi un oggetto possiede. Così, come le procedure sono imprescindibili dalla struttura dei dati, i metodi sono parti integranti e inseparabili degli oggetti.

(10)

1.10 Calcolo base sugli oggetti: Sigma

Il lavoro qui descritto prende spunto dal calcolo ad oggetti Sigma presentato da Luca Cardelli[6]. Descriviamone molto brevemente le caratteristiche.

Come il puro λ-calcolo, il σ-calcolo consiste di un set minimo di costrutti sintattici e regole di calcolo. Gli oggetti ne costituiscono l’unica struttura computazionale. Un oggetto è semplicemente una collezione di attributi con nome; tutti gli attributi sono metodi. Ogni metodo ha una variabile legata che rappresenta il self e un corpo che produce un risultato. Le uniche operazioni sugli oggetti sono le invocazioni e gli update. Fatta eccezione per le variabili, non ci sono altri costrutti nel linguaggio. In aggiunta ai metodi, in Sigma è implicitamente nota la nozione di campo. Infatti i campi non fanno parte del calcolo, ma un metodo che non usa le sue variabili può essere considerato tale. Un metodo che usa le sue variabili è anche chiamato un proper-method. Con queste convenzioni, un’invocazione di metodo può essere vista come una field selection, e una update come una field update.

1.11 Calcolo esteso sugli oggetti: Sigma_X

Sigma_X, sviluppato da M. Bellia e M. E. Occhiuto in [1] e [2], è una diretta estensione del calcolo Sigma descritto nel precedente paragrafo. Nasce dalla necessità di dare un impronta Higher-Order al calcolo ad oggetti. Procediamo gradualmente. Cosa si intende per Higher-Order ? L’ HO è considerato la principale metodologia di programmazione dei linguaggi funzionali. In tale classe di linguaggi infatti, i programmi sono funzioni e le funzioni sono valori first class del linguaggio. Questo significa che le funzioni possono essere passate come parametri ad altre, ed inoltre possono essere valori nelle strutture dati. I benefici che si ottengono da uno stile di programmazione HO sono nell’espressività del codice, che diventa estremamente più conciso, chiaro, ben strutturato e facilmente riusabile (l’esempio lampante è la

(11)

differenza abissale che c’è fra l’inferenza implementata in Java e quella riportata nella Appendice C in stile funzionale).

In un certo senso, anche i linguaggi OO sono originati da un tentativo di aggiungere funzionalità HO ai linguaggi imperativi. Infatti, in questo caso gli oggetti sono valori first class del linguaggio. Essi contengono valori (variabili d’istanza) e metodi (metodi d’istanza), entrambe legati a nomi. I metodi in questo caso, non costituiscono dei valori, ma sono contenuti negli oggetti che sono valori, e poiché possono essere passati come parametri, permettono di restituire come risultato del passaggio l’oggetto che contiene il metodo. Da questo punto di vista il linguaggio fornisce una sorta di Higher-Order che ci può aiutare a scrivere qualche programma HO-like, ma è ben poca cosa rispetto alla metodologia in se.

Sigma_X estende il paradigma OO con il passaggio dei metodi (come parametri) e l’estrazione. Per far questo introduciamo il concetto di sussunzione. Essa esprime il fatto che un oggetto di una classe è anche oggetto della sua superclasse, poiché può essere usato in ogni contesto in cui un oggetto della superclasse è usato. In altre parole, un oggetto potrebbe avere anche il tipo di ogni oggetto della sua superclasse. In particolare un metodo, una volta estratto da un oggetto, ha il tipo (corretto) restituito dalla sua invocazione dall’oggetto della superclasse.

L’ uso dei metodi come parametri, richiede due meccanismi distinti: il passaggio vero e proprio e l’estrazione.

Il passaggio di un metodo consiste nel riferire, all’interno di un loro corpo, ad una o più astrazioni che sono state già istanziate, legate, a, possibilmente differenti metodi, ogni volta che il corpo è valutato. Per far questo Sigma è stato esteso nel seguente modo:

Ÿ Parametri nei metodi oltre al self.

Ÿ Passaggio dei parametri nell’invocazione dei metodi. Ÿ Parametri attuali.

(12)

Ÿ Parametri formali.

Ÿ Passaggio dei metodi come parametri.

L’ estrazione di un metodo consiste nel selezionare un frammento di codice in un programma, e di astrarlo in un nuovo metodo che ha lo stesso (osservabile) comportamento.

Ci riferiremo, come appare ovvio, a Sigma + le estensioni presentate in precedenza con il nome di Sigma_X.

1.12 Sigma_X: grammatica con estensione per il sistema dei tipi

Per ragioni di completezza e chiarezza dell’argomento trattato riportiamo per intero la definizione grammaticale in stile Cup-like (ricordiamo che lo strumento usato per la definizione del parser è stato JavaCup) di Sigma_X con il suo sistema dei tipi.

Convenzione: in minuscolo sono indicati i simboli non terminali, in maiuscolo i simboli terminali.

(1) root ::= term |typeDecl term

(2) typeDecl ::= Type TypeIDE EQ types epyT

| Type TypeIDE EQ types SEMI typeDecl (3) TypeIDE ::= IDE:i

(4) term ::= ide |con |inv |upd |let |obj |extr (5) ide ::= IDE

(6) con ::= TKINT |TKREAL |TKCHAR |TKSTRING |TKBOOL (7) obj ::= LBRACK objComp objCompList |LBRACK RBRACK (8) objCompList ::= COMMA objComp objCompList |RBRACK (9) objComp ::= IDE EQ abst

(13)

(12) extr ::= obj EXT obj DOT IDE actList

(13) abst ::= BINDER LPAREN RPAREN terme | BINDER LPAREN IDE parList terme

| BINDER LPAREN IDE COLON types parList terme (14) parList ::= COMMA IDE parList

| RPAREN

| COMMA IDE COLON types parList

(15) actList ::= LPAREN ro metActList |LPAREN RPAREN (16) metActList ::= COMMA ro metActList |RPAREN (17) upd ::= term DOT IDE UPDATE LBRACK ro

| term DOT IDE UPDATE LBRACK ro COLON types RBRACK (18) let ::= LET IDE EQ term IN term

| LET IDE COLON types EQ term IN term

Definizioni grammaticali per il sistema dei tipi:

(1) types ::= OTypes |MTypes

(2) OTypes ::= IDE |LBRACK sel COLON MTypes OTList |Atom (3) Atom ::= STRING |INT |REAL |CHAR |BOOL

(4) sel ::= IDE

(5) OTList ::= COMMA sel COLON MTypes OTList |RBRACK (6) MTypes ::= OTypes MTypesC Otypes

(7) MTypesC ::= MTypes MTypesC |ARROW

Precedenze fra gli operatori (dalla maggiore alla minore):

• Associa a sinistra LET

• Associa a sinistra EXT, DOT, UPDATE • Associa a sinistra BINDER

Riferimenti

Documenti correlati

Dipartimento di Informatica Università “La Sapienza”.

public void draw(Window w, Position where){…}. public

 produce un errore in esecuzione, perch´e il metodo utile non `e definito nella classe Xman. Domanda 9 Data la dichiarazione Xman colosso = new Xman(); l’invocazione del

 produce un errore in esecuzione, perch´e il metodo utile() non ´e definito nella classe Papero. Beh, non fatevi confondere dal fatto che paperinik si atteggia a SupeEroe :-).

• Richiede tuttavia di un processo di traduzione da Assembly a linguaggio macchina, ma a differenza dei linguaggi ad alto livello la corrispondenza è 1:1 (ad ogni istruzione

quando viene istanziato da un altro oggetto della stessa classe. l’argomento è una reference ad un

 Atipici: es: i fogli elettronici possono essere considerati linguaggi di programmazione in cui, in una certa misura, le relazioni temporali sono sostituite da relazioni spaziali

bool operator<(const Integer& left, const Integer& right) { return left.i <