Newsgroup: it.comp.lang.c++
Titolo: Polimorfismo parametrico e polimorfismo intensionale

 magari tenendoti su un piano non strettamente tecnico?
Il senso generale del discorso è proprio quello che diceva Enrico Maria
Giordano, ovvero che i template in C++ sono il supporto per la
programmazione generica e non per quella ad oggetti.
Entrambe consentono il riuso e l'estensione del codice però partono da
presupposti totalmente differenti, ingenerando confusione se non si sono
capiti prima questi. Ne escono stili di programmazione (e di design)
decisamente in conflitto tra loro. Nel contesto della discussione alla quale
mi riferivo, si dibatteva a proposito della difficoltà di scrivere codice
C++ *ad oggetti* e di supportare il riuso e l'estensione in modo flessibile
e sicuro. Flessibile cercando di ridurre il più possibile la ricompilazione
del codice preesistente; sicuro nel senso di garantire il corretto
funzionamento dell'insieme quando si aggiunge una parte, il che significa
mantenere una semantica uniforme nel contesto dell'intera gerarchia di
classi quando si deriva una nuova classe. Senza contare i problemi tipo "la
fragile base class" alla base della progettazione di gerarchie
d'ereditarietà. Insomma, scrivere codice riusabile, modificabile,
estendibile e aderente alle specifiche non è certo facile, neanche in
un'ottica OO, però il confondere l'approccio ad oggetti con quello generico
e, soprattutto, l'attribuire a quest'ultimo la causa delle difficoltà del
primo evidenzia una certa confusione o almeno una comprensione parziale. Il
C++, poi, non aiuta, visto che supporta entrambi i paradigmi e molte persone
imparano il paradigma a partire dal linguaggio. Nel caso in questione,
sapendo che il C++ è un linguaggio orientato ad oggetti, erroneamente
ritengono che le sue caratteristiche e i suoi limiti siano anche quelli
dell'OOP e questo è solo l'inizio dei guai.
Andrea

 che cosa di essi crea problemi?
Per quello che mi viene in mente, i problemi più comuni sono evidenziabili a
livello di:
1) stile di programmazione;
2) modello implicito basato sull'istanziazione, almeno per quello che
riguarda il C++;
3) definizione di una specifica (deriva in ultima analisi da 2));
4) dimensioni del codice;
5) dichiarazioni multiple (deriva da 2) anch'essa).
Partiamo dal basso verso l'alto.
5) Dichiarazioni multiple.
Qualora ci sono funzioni overloaded nel template, sorgono problemi dovuti
alle potenziali dichiarazioni multiple. Consideriamo il seguente codice:
template <class T> class TMultDecl
{
public:
void f (B b)    {...}    // B è una classe definita altrove
void f (int n)    {...};
void f (T a)    {...};
}
Le ultime due dichiarazioni non verranno compilate perché viene rilevata una
dichiarazione multipla:
TMultDecl<char> charTemplate;
TMultDecl<B> bTemplate;    // void f (B b) "in conflitto" con void f (T a)
TMultDecl<int> intTemplate; // void f (int n) "in conflitto" con void f (T
a)
E' ovvio che il problema è dovuto al modello di istanziazione del template.
Per maggiori informazioni e per una tecnica utile ad ovviare, almeno in
parte al problema, trovi qualcosa dal testo "C++ manuale di stile"
di C.Pescio, dal quale, salvo una piccola aggiunta, l'esempio è stato tratto
(il capitolo è l'11).
4) Dimensioni del codice.
Questo è ovvio. Il compilatore genererà per ogni nuova istanza presente nel
sorgente l'istanziazione di tutto il codice relativo al template. Si porta
dietro tutti i problemi della replicazione del codice, in primis un
eseguibile piuttosto.
3) Definizione di una specifica (deriva in ultima analisi da 2)).
Il problema è quello di definire una specifica non ambigua che sia valida
per tutto il codice generico istanziabile. Si arriva alla tendenza della
sovra-generalizzazione che potrebbe portare a inconsistenze a seconda del
tipo di istanziazione che se ne fa e a seconda dell'ambiente ove viene
effettuata. Anche qui, lo stesso volume del Pescio è utile per vedere esempi
ed opportune contromisure.
2) Modello implicito basato sull'istanziazione.
A mio avviso è il nodo cruciale di tutta la baracca. Possiamo analizzarlo a
due livelli semantici a seconda che ci soffermiamo sul riuso oppure
sull'estensione del codice.
Dal punto di vista del riuso abbiamo un'implementazione del modello di
istanziazione dei template che segue la scuola implicita a favore di una
maggiore flessibilità, ma anche di un aumento in complessità e in una
perdita in sicurezza. La radice dei problemi è sempre quella di basare la
semantica di un comportamento esclusivamente sulla signature della funzione,
come già discusso nel thread "C++ vs. Java" a proposito delle interfacce.
Salvo casi banali, dalla dichiarazione di una funzione non è evidente nè la
sua semantica, se non in termini intuitivi e quindi ambigui, nè se questa è
applicabile al tipo correntemente istanziato. Le discrepanze in fase di
esecuzione sono talvolta difficili da individuare soprattutto perché non è
noto a priori l'insieme massimale di possibili istanziazioni. Il
compilatore, in
genere, non è d'aiuto perché si limiterà ad istanziare per tentativi e il
primo matching che riuscirà a trovare verrà istanziato. Non ci sono
verifiche semantiche, quindi è il programmatore a dover occuparsi dei
vincoli impliciti che il linguaggio nasconde. Qui si cammina sul ghiaccio...
Dal punto di vista dell'estensione del codice, invece, si parte da un
modello che definisce una funzione i cui argomenti sono parametrici. Ancora
una volta, restano implicite le proprietà dei tipi attuali perché tali lo
sono quelle del parametro formale T. Incorporare le proprietà dei dati in un
parametro formale generico non è un modo pulito e sufficientemente chiaro
per esprimere una semantica non ambigua. Senza menzione per i problemi di
estensione che richiedono sovente una massiccia ricompilazione. Questo modo
di procedere è in forte contrasto con il polimorfisco ad oggetti nel quale
anziché definire una funzione parametrica si preferisce definire l'insieme
di tutti e soli i tipi per i quali valgono certe proprietà. L'ereditarietà
fornisce il consueto supporto a questo modo di ragionare e permette di
esplicitare, attraverso la struttura di una gerarchia di classi, tutte le
astrazioni che sono soggette a una certa semantica (pur restando immutate le
limitazioni riconducibili al design by contract deducibile solo dalle
signature). In tale contesto, una classe derivata eredita i comportamenti
della classe base, salvo opportune ridefinizioni. Molto del suo
comportamento viene dichiarato dalla classe base in modo esplicito o
esplicitamente ridefinito in loco.
1) stile di programmazione.
Lo stile riguarda soprattutto le pratiche più o meno rischiose che il
polimorfismo parametrico incoraggia, confrontandolo con quello intensionale
(o per inclusione) tipico della OOP. Anche qui, più ad alto livello, il
confronto è tra scuola implicita e scuola esplicita. Lavorare con assunzioni
implicite significa supportare la flessibilità, ma anche non dichiarare
formalmente le proprie dichiarazioni d'intenti, i propri vincoli e le
proprie assunzioni, lascaindo delle ambiguità che causano grossi problemi di
manutenzione ed estensione, oltre che di utilizzo. Il linguaggio in tal
senso non dà alcun supporto e il design che ne deriva è spesso fatto a
braccio o per tentativi. Il maggiore problema di questo approccio si
evidenzia al crescere della complessità, quando un mucchio di dettagli
sfuggono e non si capisce esattamente che cosa succede (e neppure che cosa
dovrebbe succedere :-( ). In quest'ottica, la natura di STL e del codice
polimorfo parametrico in generale è radicalmente opposta a quella della OOP
e del polimorfismo intensionale.
Il post non vuole essere un rifiuto totale verso i template, che io continuo
ad usare, soprattutto come contenitori. Volevo sottolineare solo che le
caratteristiche di un approccio si riflettono sullo stile di programmazione,
portando benefici e rischi di cui bisognerebbe essere sempre consci nel
momento in cui si prendono determinate scelte, anziché celarsi più o meno
beatamente nell'ambiguità del linguaggio, nell'insieme di tool che si usano
o nelle abitudini che si hanno (chiedo scusa per l'affermazione un po'
forte).
Mi scuso infine per la lunghezza del post stesso, sperando di non aver
annoiato e, ancor di più, di non aver detto scemenze.
Ciao
Andrea

 annoiato e, ancor di più, di non aver detto scemenze.
Magari tutti i post fossero così invece che "Come faccio a fare il debug
con il VC++?" (è solo un esempio, non se la prenda l'autore di quel
messaggio). Sono comunque d'accordo, anche se non ho molta esperienza
pratica sull'argomento, su tutto quello che hai scritto.
EMG

imparano il paradigma a partire dal linguaggio.
Come ho gia` avuto occasione di dire ad altri, IMO il supporto a piu`
paradigmi e` un punto di forza e contemporaneamente un punto debole
del C++.
E` un punto di forza perche` consente una piu` ampia scelta di strumenti
ed e` un punto debole perche` il supporto a strumenti radicalmente diversi
rende piu` complesso il linguaggio e i compilatori e toglie risorse utilizzabili
per migliorare il supporto ad uno specifico paradigma.
Paolo
-----------------------------------------------------------------
  Paolo Marotta
  Computer Science, University of Pisa
  home:   
http://www.cli.di.unipi.it/~marotta
-----------------------------------------------------------------

Andrea


e del polimorfismo intensionale.
Il polimorfismo parametrico (in C++: overloading) e' una caratteristica
tipica dell'OOP.
Strachey ha dato per primo la definizione di polimorfismo, in seguito
Cardelli and Wegner hanno fatto un'altra classificazione aggiungendo
la definizione di "inclusion polymorphism" (in C++: overriding).
Il polimorfismo parametrico e' supportato anche da linguaggi OOP che non
hanno classi parametriche o altre forme di programmazione generica.
Fra questi c'e' SmallTalk, che non essendo tipizzato non ha alcun bisogno
di tipi parametrici, e Common Lisp Object System (CLOS).
Ti consiglio un'interessante lettura:
"Genericity versus inheritance", un'articolo di Bertrand Mayer
presentato alla prima conferenza OOPSLA (1986).
In questo articolo, Mayer, dimostra come sia possibile sostituire
il meccanismo della genericita' con la derivazione e di quanto
sia impossibile fare l'inverso.
In tale articolo si nota anche quanto la sostituzione radicale della
genericita' sia macchinoso.
David Flanagan in "Java in a Nutshell" suggerisce di usare il type cast
per ottenere l'oggetto da una classe container. Questo perche' in Java
non e' supportato il meccanismo della genericita' e sarebbe uno sforzo
eccessivo specializzare contenitore per contenitore per ottenere
un controllo sui tipi.
Lo stesso Meyer arriva alla conclusione che usare derivazione e genericita'
aiuta alla costruzione di software flessibile e robusto.
Sempre Meyer in "Object-oriented Software Construction" afferma che la
genericita', nonstante sia una caratteristica di linguaggi non Object
Oriented come ADA, e' una parte essenziale di un linguaggio OOP.
Grady Booch considera il supporto delle classi parametriche
complementare alla derivazione.
Considero STL la prima libreria C++ strutturata con criterio scientifico
(non per questo senza difetti).
La struttura ortogonalizzata di tale libreria, ottenuta grazie all'uso
intensivo dei template, permette il riutilizzo del codice in modo molto
superiore di quanto si sarebbe potuto ottenere con l'uso esclusivo delle
altre caratteristiche del linguaggio.
Vorrei capire meglio con esempio il modo in cui STL crea complessita'
nel riuso e nell'estensione.
La complessita' cresce anche al crescere della quantita' di codice
necessaria per raggiungere una soluzione. Ogni riga di codice e'
potenzialmente una fonte d'errore.
Oreste Notelli

aiuta alla costruzione di software flessibile e robusto.
Sicuramente i template sono di aiuto al C++ tuttavia essi devono gran
parte della loro importanza per via di limiti nel modello di OOP del linguaggio.
(vedi articolo di Pescio)
Paolo


un qualsiasi tipo di valori...
Infatti, sono le stesse conclusioni a cui è arrivato B. Meyer in risposta
alle affermazioni/accuse di Stepanov...
Andrea

nel riuso e nell'estensione.
No, qui forse non mi sono spiegato io. Il signore con il quale discutevo di
queste cose, si lamentava delle difficoltà di riuso e di estensione delle
gerarchie di classi basate su ereditarietà (problema noto, per molti versi),
attribuendo la responsabilità di tali guai ad STL e ai template (conclusione
errata). Tutto è partito da qua. Per far capire che i problemi derivanti
dalla fragile base class & Co. non c'entravano nulla con la programmazione
generica è nato il dibattito tra polimorfismo intensionale e parametrico.
Per quello che concerne poi questa discussione, STL essendo basata
fortemente sui template, non si sottrae alla critica di incoraggiare un
design by contract esplicito. La questione è filosofica, è di stile ed è di
approccio. Io preferisco avere assunzioni esplicite, anche se queste
finiscono con il legarmi le mani in certe occasioni, limitando la mia
libertà. Però ritengo che questo sia un prezzo ragionevole da pagare per
avere un controllo maggiore (ciò che è esplicito può essere restrittivo, ma
è anche verificabile, deterministico e palese: è una dichiarazione di
intenti a priori, pensiero questo più in linea con lo stile C++ che con
quello di SmallTalk - emblema di due modi di interpretare il paradigma ad
oggetti diametralmente opposti).
Ciao
Andrea


Recuperero' l'articolo di cui parli, e forse cerchero' anche il libro a
cui accennava Andrea Baruzzo: ho una ottima considerazione dell'autore.
Oreste Notelli


Non capisco a quale linguaggio tu ti riferisca...
Oreste Notelli

alle affermazioni/accuse di Stepanov...
Non e` stato l'unico... in effetti Stepanov (senza togliere alcun
merio a lui ed al suo lavoro) ha piu` volte dimostrato di non aver
compreso la OOP e di confonderne le idee con la particolare
implementazione del C++ (vedi interviste di C. Pescio pubblicate
su CP in numeri anteriori al 62).


Non capisco a quale linguaggio tu ti riferisca...
Nessun linguaggio in particolare...
Molti (anche se non tutti) problemi del C++ derivano dal fatto
che i tipi char, int, float ecc.  non sono classi e sopratutto dal fatto che
manchi una classe TObject da cui tutti gli altri tipi derivano.
In un contesto simile sarebbe as es. possibile avere una lista contenente
puntatori a TObject assolutamente riusabile e che non richieda
minimamente l'uso dei template. In questo senso i template hanno
nel C++ un ruolo maggiore di quanto non avrebbero in altri linguaggi.
Altri problemi sono descritti nell'articolo di Pescio segnalato in un
mio precedente post che qui per brevita` non riporto. Se sei interessato
cerchero` di riassumerlo e postarlo in un prossimo intervento.


cui accennava Andrea Baruzzo: ho una ottima considerazione dell'autore.
Il libro puo` essere acquistato solo direttamente da Infomedia (48.000 lire)
e` un manuale di stile (anche se in taluni punti va oltre) che parla dell'importanza
che ha avere e seguire regole precise di codifica. Il libro fornisce una
raccolta di consigli e in taluni punti va oltre i problemi di stile (tipo quando
parla del problema della fragile class).
Per inciso ho anch'io un'ottima stima dell'autore...


design by contract esplicito.
   volevo scrivere "design by contract implicito". Sorry
Andrea

modello OO DEL C++ (e non della oop in generale).
Sicuro. Esprimere delle valutazioni di carattere generale lascia sempre
spazio per le eccezioni. Poi, come penso avrai capito, non ne facevo una
questione "Template contro tutti". Ritengo interessante e stimolante
confrontarsi, invece, sulle differenze dei due approcci (inplicito ed
esplicito), magari anche con qualche esempio per mostrare le ripercussioni
sullo stile di programmazione, sulle priorità di un progetto, sulla capacità
di "governare" l'intero processo di sviluppo etc. Se i problemi di una
qualsiasi caratteristica di un qualsiasi linguaggio non diventano rilevanti
in una data situazione, è soltanto sciocco non sfruttarne le comodità. Non
ti sembra?
Andrea

...
Sulla differenza di pensiero tra quello che tu chiami "scuola esplicita" e
"scuola implicita", hai senza dubbio ragione tu ad affermare che la prima
garantisce maggior controllo e sicurezza.
ADA e' infatti nato per limitare al minimo gli errori di codifica.
Oreste Notelli

nel C++ un ruolo maggiore di quanto non avrebbero in altri linguaggi.
Creare una lista di oggetti dello stesso tipo senza usare
classi parametriche comporta diverse difficolta', a seconda
del linguaggio usato.
1. Linguaggi non tipizzati (SmallTalk)
 Non esiste nessun controllo statico, quindi in una
 lista di oggetti ti tipo Rettangolo, non c'e' il supporto
 del compilatore ad impedire di inserirvi oggetti di
 tipo Cerchio, Cubo, Farfalla, ecc.
 Il controllo avverra' a run-time e non e' neanche
 gratuito, sei costretto ad implementarlo nella lista.
2. Linguaggi tipizzati.
      Per mantenere il controllo statico dei tipi e' necessario
      specializzare l'interfaccia richiesta come contratto dalla lista.
      (Listable).
      E questo dovremmo farlo per ogni oggetto che vorremmo inserire
      in una lista.
      E non e' sufficente, dovremo specializzare anche un tipo lista
      per ogni tipo Listable gestito: ListaFarfalle, ListaCubi, ecc.
Funzioni come max non sarebbero implementabili se non con linguaggi che
permettono di restringere tramite derivazione, le funzionalita' e
l'insieme dei tipi gestiti nei tipi (nelle classi) base.
Ovverro, linguaggi che supportano il covariance e il descendent hiding.
Rimarrebbe, comunque, da implementare la specializzazione dell'interfaccia
usata dalla funzione max (Comparable).
Oreste Notelli

template di max che la macro (Borland C++ 5.02)
Hai ragione, ed in qualsiasi compilatore C++.
Gli operatori di conversione a const char* faranno si' che
max effettui una confronto fra puntatori. Che non e' certamente
quello che si vuole.
Oreste Notelli

usata dalla funzione max (Comparable).
Mi ripeto, la mia non era una condanna totale dei template, semplicemente essi
nel C++ IMO hanno un ruolo maggiore di quanto in generale avrebbero in
un altro linguaggio (con ovviamente caratteristiche diverse).
E` in quest'ottica che devi leggere le mie affermazioni.


ti sembra?
Certamente... oltretutto io amo le comodita`.


e quindi maggiore complessita' da gestire, questo soprattutto in C++.
Si, capisco la tua osservazione. E infatti penso che non sia sensato
sostituire -tuot cur- la programmazione generica con la derivazione, così
per partito preso.  Bisognerebbe fare,prima, le proprie valutazioni e poi,
in base a una serie di fattori, decidere. Come dici giustamente tu, il gioco
potrebbe non valer la candela.
Un ultimo spunto che mi viene in mente: sarebbe opportuno "pesare" anche la
complessità. Mi spiego: non tutta la complessità è dello stesso tipo. Penso
ai vincoli di natura computazionale, a quelli di natura spaziale, per
arrivare a quelli semantici, come la comprensibilità di una soluzione o la
resilienza alle modifiche di un design etc. E' naturale che, template o non
template, derivazione o non derivazione, java o c++, linux o nt (mettici qui
la prossima guerra religiosa che ti viene in mente), certe cose trovano un
riscontro naturale più con uno strumento che con un altro.
Ciao
Andrea

Con la lista che descrivi perderesti pero' la tipizzazione forte: una
lista di TObject non e' poi cosi' diversa da una lista di void * ...
Nicola Musatti

all'oggetto da inserire.
??Aggiungendo semplicemente una funzione virtual??
Se vuoi mantenere il controllo STATICO dei tipi devi
per forza cambiare i prototipi di TUTTI i metoodi che
trattano l'elemento listable
Esempio in Eiffel (con covariance):
class List feature
    item_listable : Listable
    implementation: ARRAY[Listable]
    -- funzione di inserimento di un'elemento
    put (x: Listable) is
    do
        --
    end
    find (index: INTEGER) : Listable is
    do
        --
    end
    -- tutte le altre funzioni
end
class IntegerList inherit
        List
            redefine item_listable,
                     implementation,
                     find,
                     put
            end
        feature
    implementation:
    item_listable: IntegerListable
    implementation: ARRAY[IntegerListable]
    -- funzione di inserimento di un'elemento
    put (x: IntegerListable) is
    do
    end
    find (index: INTEGER): IntegerListable is
    do
        Result := implementation[index]
    end
end
Esempio in C++ (con antivariance)
class List
{
    // scusa l'uso del template, ma...
    // per l'esempio non ha rilevanza
    vector<Listable*> implemetation;
public:
    put(Listable* pListable)
    {
        /*...*/
        implemetation.push_back(pListable);
        /*...*/
    }
    Listable* find (index)
    {
        return implemetation[index];
    }
};
class IntegerList : public List
{
public:
    put(IntergerListable* pListable)
    {
        List::put(pListable);
    }
    IntergerListable* find (index)
    {
        return static_cast<IntergerListable*>
            (List::find(index));
    }
};
Come vedi, se voglio mantenere il controllo sui tipi
devi ridefinire ogni parte dell'interfaccia doce
compare Listable. In piu', in Eiffel per ottenere l'effetto
di covarianza, devo fare del lavoro in piu'... ma questo
e un'altro discorso, come e' un'altro discorso se ha senso la
covarianza in un linguaggio tipizzato (l'unica pecca di Eiffel).
Ma la mia osservazione verte proprio in questo:
NON ESISTE alcun linguaggio object oriented che ci
permetta di definire un tipo lista in grado di contenere
qualsiasi oggetto con controllo statico sui tipi.
E' necessario derivare un classe listXXX da un tipo base
"lista" per ogni XXX tipo di elemento che si vuole listare
E comunque con controllo statico sui tipi o senza, il tutto
lo si puo' fare benissmo in C++, senza scomodare
Eiffel, Oberon, Modula-3, Trellis, Sather, Beta, Self, e
tutti quei linguaggi OOP puri che vivono solo in ambienti
accademici e/o sperimentali.
Oreste Notelli

lista di TObject non e' poi cosi' diversa da una lista di void * ...
Non e` del tutto esatto...
In un linguaggio con type checking statico la cosa sarebbe effettivamente
molto simile (anche se qualcosa sarebbe possibile farla), non
sarebbe un problema invece in presenza di type checking dinamico.
E comunque i template non sono una soluzione all'interno di un linguaggio
che segua la "filosofia tradizionale" della OOP inclusiva (in cui un puntatore
alla classe base puo` cioe` puntare ad un oggetto di classe derivata).
In tali contesti la soluzione sarebbe unicamente il contenimento diretto.
La situazione che avevo derscritto io in sostanza richiederebbe un
modello diverso da quello del C++, che fornisca ovviamente un minimo di
supporto. Per inciso non e` un caso che in C++ sia stato introdotto
il dynamic_cast<> (e tutto cio` di cui esso necessita).
In effetti OOP e Generic Programming sono risposte diverse ad uno
stesso problema con vantaggi e svantaggi diversi. Ad esempio
in un linguaggio con type checking statico e in cui un puntatore
a classe base non puo` puntare a classe derivata, la programmazione
generica ti permetterebbe di scrivere una sola volta la lista, ma
dovresti istanziarne una diversa per ogni tipo, inoltre non
potresti avere liste di oggetti appartenenti ad una gerarchia (polimorfi) da
trattare uniformemente come oggetti di classe base.
La OOP inclusiva invece ti permette di avere una unica lista 
(sia a livello di codice che di istanze) per oggetti
polimorfi da trattare uniformemente (o ove il linguaggio lo consenta,
tra cui scegliere quello del tipo "giusto") al prezzo imposto dalle
funzioni virtual (o da un type checking dinamico).
Poi come spesso accade molti problemi non sono impliciti in un
modello, ma lo sono in virtu` della specifica implementazione.


accademici e/o sperimentali.
Smalltalk non mi pare che operi solo in ambienti accademici e
comunque il fatto che molti di questi linguaggi operino
solo in ambiente accademico non significa che ci sia qualcosa
che non vada... Anche i linguaggi OOP e la programmazione
generica per molto tempo sono rimasti vincolati in ambito
accademico e/o sperimentale, cosi come le interfacce GUI.
Il passaggio verso i "comuni mortali" viene deciso solo nel momento in
cui le aziende non riescono piu` ad ottenere molti guadagni dai
loro vecchi (ma assodati) prodotti.
