Tuesday, May 30, 2006
C# 3.0 got open classes
Still, C# is in constant flux :-) and in the 3.0 specification we have a concept of "extension methods", which is just another way to say "open classes". Finally, I do have a nice solution to many problems. Indeed, for a long while I entertained the idea of adding this feature to C++ (hacking on GCC) as a proof of concept. Well, someone else did it for me (though for a different language).
I should spare a little time to write something more comprehensive on the subject. Meanwhile, if you need a little inspiration on why this stuff can be useful, see my Clone Vs. Assign post: now I can add a ShallowAssign method to System.Object. This is just a small part of the picture: Meyers' insight on encapsulation provides another interesting perspective on how to use open classes (or, as I'll call them from now on, "extension methods" :-).
Comments:
<< Home
Invece di aggiungere al linguaggio costrutti limitati e contorti come gli extension methods, non sarebbe meglio rinunciare del tutto al concetto di classe (al solo concetto di classe, non all'object orientation)?
Mi spiego meglio: supponiamo di avere una classe di questo tipo:
class Point
{
public Point(double anX, double anY)
{
x = anX;
y = anY;
}
public double getX()
{
return x;
}
public double getY()
{
return y;
}
public void setX(double val)
{
x = val;
}
public void setY(double val)
{
y = val;
}
public double distanceFromOrigin()
{
return sqrt(x*x + y*y);
}
private double x;
private double y;
};
e supponiamo di avere un linguaggio in cui la classe di cui sopra si possa scrivere cosi':
module Geometry
{
public type Point
{
private x;
private y;
}
public Point point(double anX, double anY)
{
return new Point(x = anX, y = anY);
}
public double getX(Point p)
{
return p.x;
}
public double getY(Point p)
{
return p.y;
}
public void setX(Point p, double val)
{
p.x = val;
}
public void setY(Point p, double val)
{
p.y = val;
}
}
double distanceFromOrigin(Point p)
{
double x = getX(p);
double y = getY(p);
return sqrt(x*x + y*y);
}
avremmo una sezione chiamata "module Geometry", il cui unico scopo e' quello di "nascondere" tutto quello che, al suo interno, e' dichiarato con visibilita' privata. In questo caso, per esempio, il tipo "Point" e' pubblico, e puo' essere utilizzato anche all'esterno della modulo, mentre i suoi campi "x" ed "y" sono accessibili solo al suo interno. I metodi getX, getY, setX, setY sono nella stessa encapsulation_unit che contiene il tipo Point, e quindi possono accedere direttamente ai suoi campi. Il metodo distanceFromOrigin, che puo' essere implementato senza accedere direttamente ai campi privati di Point, puo' benissimo essere spostato all'esterno del "module Geometry". Ora, se solo il linguaggio consentisse di usare la sintassi:
object.method(p1, p2, ... , pn);
come "syntactic sugar" per
method(object, p1, p2, ... , pn)
il metodo distanceFromOrigin potrebbe essere riscritto cosi':
double distanceFromOrigin(Point p)
{
double x = p.getX();
double y = p.getY();
return sqrt(x*x + y*y);
}
con la stessa sintassi usata per i liguaggi basati su classi. Si potrebbe andare un po' la' e considerare anche
public type SomeType
{
private int someField;
public int someMethod()
{
return 10 * someField;
}
}
come semplice "syntactic sugar" per
public type SomeType
{
private int someField;
}
public int someMethod(SomeType this)
{
return 10 * this.someField;
}
In questo modo la classe di cui sopra potrebbe essere riscritta in questo modo:
module Geometry
{
public type Point
{
private x;
private y;
public double getX(Point p)
{
return x;
}
public double getY(Point p)
{
return y;
}
public void setX(Point p, double val)
{
x = val;
}
public void setY(Point p, double val)
{
y = val;
}
}
public Point point(double anX, double anY)
{
return new Point(x = anX, y = anY);
}
}
double distanceFromOrigin(Point p)
{
double x = p.getX();
double y = p.getY();
return sqrt(x*x + y*y);
}
Dato che il costrutto "module" sarebbe usato solo per definire una "unita' di incapsulazione" (la funzione di definire dei namespace potrebbe essere delegata ad un'altra parola chiave, come "namespace" o "package"), si potrebbe pensare di non assegnargli nemmeno un nome. Il codice diventerebbe questo:
namespace Geometry
{
module
{
public type Point
{
private x;
private y;
public double getX(Point p)
{
return x;
}
public double getY(Point p)
{
return y;
}
public void setX(Point p, double val)
{
x = val;
}
public void setY(Point p, double val)
{
y = val;
}
}
public Point point(double anX, double anY)
{
return new Point(x = anX, y = anY);
}
}
double distanceFromOrigin(Point p)
{
double x = p.getX();
double y = p.getY();
return sqrt(x*x + y*y);
}
// more stuff here...
}
I moduli cosi' creati potrebbero contenere l'equivalente di una sola classe, o solo parte di essa, nel caso alcuni metodi possano essere implementati senza accedere direttamente ai campi privati di un dato tipo (e rendendo in questo modo l'incapsulazione piu' forte). Oppure potrebbero contenere piu' tipi strettamente legati tra di loro, rendendo cosi' inutile un costrutto come "friend". In questo modo non servirebbe nessun costrutto speciale per aggiungere metodi ad una classe (anche metodi virtuali), e si avrebbero anche tanti altri vantaggi: si potrebbero implementare i multi-dispatch methods (che non vedo come potrebbere essere implementati con l'approccio basato su classi) e non ci sarebbe piu' bisogno di avere costruttori (con tutti i problemi e le limitazioni che questi comportano, almeno in linguaggi come C++, Java e C#) o di metodi statici, che potrebbero essere rimpiazzati da normali funzioni. Si avrebbe anche piu' flessibilita' da un punto di vista sintattico: ad esempio, invece di scrivere:
Point p1 = ...
Point p2 = ...
double d = p1.distance(p2);
si potrebbe scrivere
double d = distance(p1, p2);
che, personalmente, mi sembra piu' naturale. Per come la vedo io, il linguaggio ne risulterebbe sia potenziato che semplificato.
Mi sembra, insomma, che ci sia tutto da guadagnare e niente da perdere nel rinunciare alle classi e a sostituirle con costrutti specializzati: namespaces (per evitare conflitti tra nomi), moduli (per l'incapsulazione), tipi e funzioni. E so che esistono dei linguaggi semisconosciuti che fanno qualcosa del genere. Il fatto pero' che tutti i linguaggi piu' usati siano basati su classi mi fa sospettare che ci sia qualcosa che mi sfugge.
Che vantaggi ci sono, di preciso, ad usare le classi?
Mi spiego meglio: supponiamo di avere una classe di questo tipo:
class Point
{
public Point(double anX, double anY)
{
x = anX;
y = anY;
}
public double getX()
{
return x;
}
public double getY()
{
return y;
}
public void setX(double val)
{
x = val;
}
public void setY(double val)
{
y = val;
}
public double distanceFromOrigin()
{
return sqrt(x*x + y*y);
}
private double x;
private double y;
};
e supponiamo di avere un linguaggio in cui la classe di cui sopra si possa scrivere cosi':
module Geometry
{
public type Point
{
private x;
private y;
}
public Point point(double anX, double anY)
{
return new Point(x = anX, y = anY);
}
public double getX(Point p)
{
return p.x;
}
public double getY(Point p)
{
return p.y;
}
public void setX(Point p, double val)
{
p.x = val;
}
public void setY(Point p, double val)
{
p.y = val;
}
}
double distanceFromOrigin(Point p)
{
double x = getX(p);
double y = getY(p);
return sqrt(x*x + y*y);
}
avremmo una sezione chiamata "module Geometry", il cui unico scopo e' quello di "nascondere" tutto quello che, al suo interno, e' dichiarato con visibilita' privata. In questo caso, per esempio, il tipo "Point" e' pubblico, e puo' essere utilizzato anche all'esterno della modulo, mentre i suoi campi "x" ed "y" sono accessibili solo al suo interno. I metodi getX, getY, setX, setY sono nella stessa encapsulation_unit che contiene il tipo Point, e quindi possono accedere direttamente ai suoi campi. Il metodo distanceFromOrigin, che puo' essere implementato senza accedere direttamente ai campi privati di Point, puo' benissimo essere spostato all'esterno del "module Geometry". Ora, se solo il linguaggio consentisse di usare la sintassi:
object.method(p1, p2, ... , pn);
come "syntactic sugar" per
method(object, p1, p2, ... , pn)
il metodo distanceFromOrigin potrebbe essere riscritto cosi':
double distanceFromOrigin(Point p)
{
double x = p.getX();
double y = p.getY();
return sqrt(x*x + y*y);
}
con la stessa sintassi usata per i liguaggi basati su classi. Si potrebbe andare un po' la' e considerare anche
public type SomeType
{
private int someField;
public int someMethod()
{
return 10 * someField;
}
}
come semplice "syntactic sugar" per
public type SomeType
{
private int someField;
}
public int someMethod(SomeType this)
{
return 10 * this.someField;
}
In questo modo la classe di cui sopra potrebbe essere riscritta in questo modo:
module Geometry
{
public type Point
{
private x;
private y;
public double getX(Point p)
{
return x;
}
public double getY(Point p)
{
return y;
}
public void setX(Point p, double val)
{
x = val;
}
public void setY(Point p, double val)
{
y = val;
}
}
public Point point(double anX, double anY)
{
return new Point(x = anX, y = anY);
}
}
double distanceFromOrigin(Point p)
{
double x = p.getX();
double y = p.getY();
return sqrt(x*x + y*y);
}
Dato che il costrutto "module" sarebbe usato solo per definire una "unita' di incapsulazione" (la funzione di definire dei namespace potrebbe essere delegata ad un'altra parola chiave, come "namespace" o "package"), si potrebbe pensare di non assegnargli nemmeno un nome. Il codice diventerebbe questo:
namespace Geometry
{
module
{
public type Point
{
private x;
private y;
public double getX(Point p)
{
return x;
}
public double getY(Point p)
{
return y;
}
public void setX(Point p, double val)
{
x = val;
}
public void setY(Point p, double val)
{
y = val;
}
}
public Point point(double anX, double anY)
{
return new Point(x = anX, y = anY);
}
}
double distanceFromOrigin(Point p)
{
double x = p.getX();
double y = p.getY();
return sqrt(x*x + y*y);
}
// more stuff here...
}
I moduli cosi' creati potrebbero contenere l'equivalente di una sola classe, o solo parte di essa, nel caso alcuni metodi possano essere implementati senza accedere direttamente ai campi privati di un dato tipo (e rendendo in questo modo l'incapsulazione piu' forte). Oppure potrebbero contenere piu' tipi strettamente legati tra di loro, rendendo cosi' inutile un costrutto come "friend". In questo modo non servirebbe nessun costrutto speciale per aggiungere metodi ad una classe (anche metodi virtuali), e si avrebbero anche tanti altri vantaggi: si potrebbero implementare i multi-dispatch methods (che non vedo come potrebbere essere implementati con l'approccio basato su classi) e non ci sarebbe piu' bisogno di avere costruttori (con tutti i problemi e le limitazioni che questi comportano, almeno in linguaggi come C++, Java e C#) o di metodi statici, che potrebbero essere rimpiazzati da normali funzioni. Si avrebbe anche piu' flessibilita' da un punto di vista sintattico: ad esempio, invece di scrivere:
Point p1 = ...
Point p2 = ...
double d = p1.distance(p2);
si potrebbe scrivere
double d = distance(p1, p2);
che, personalmente, mi sembra piu' naturale. Per come la vedo io, il linguaggio ne risulterebbe sia potenziato che semplificato.
Mi sembra, insomma, che ci sia tutto da guadagnare e niente da perdere nel rinunciare alle classi e a sostituirle con costrutti specializzati: namespaces (per evitare conflitti tra nomi), moduli (per l'incapsulazione), tipi e funzioni. E so che esistono dei linguaggi semisconosciuti che fanno qualcosa del genere. Il fatto pero' che tutti i linguaggi piu' usati siano basati su classi mi fa sospettare che ci sia qualcosa che mi sfugge.
Che vantaggi ci sono, di preciso, ad usare le classi?
Ciao, mi decido a scrivere dopo aver seguito per un po' il blog e i commenti.
Questo e' l'ennesimo post in cui si evidenziano i limiti fondamentali di C# o C++. Per come la vedo io, il problema e' stato risolto alla perfezione in Objective C. Non sto a spiegare tutto il linguaggio qui, mi interessa solo elencare alcune possibilita' che offre, vorrei il vostro parere.
1) Le classi hanno attributi, metodi di classe e metodi di istanza (come in tutti i linguaggi a oggetti).
2) C'e' un runtime con capacita' di introspezione, ma non ha macchina virtuale.
3) Non esistono classi virtuali da usare come interfacce attraverso ereditarieta' multipla. Esistono "protocolli", ovvero dichiarazioni di metodi raggruppati sotto un nome che vengono implementati dalle classi. Ereditarieta' singola.
4) Le classi sono estendibili a piacere col meccanismo delle "categorie". Esempio: la classe Point non prevede il metodo "scale". Ok, lo aggiungo perche' mi serve. Non ho bisogno del codice di implementazione di Point, mi basta avere accesso alla sua dichiarazione. Una volta dichiarata la categoria in un file h e implementata nel mio codice, ecco che potro' chiamare il metodo "scale" esattamente come se fosse stato definito fin dal principio nella classe.
In altre parole, il runtime permette di aggiungere al volo metodi alle classi. Il tutto in modo veramente trasparente e semplice, se serve posso far vedere il codice.
Le categorie risolvono anche un problema tipico del C++, ovvero l'impossibilita' di estendere una classe distribuita in un framework linkato dinamicamente.
Lo conoscevate gia'? Vi ha incuriosito? Ditemi che ne pensate.
Ciao
Marco
Questo e' l'ennesimo post in cui si evidenziano i limiti fondamentali di C# o C++. Per come la vedo io, il problema e' stato risolto alla perfezione in Objective C. Non sto a spiegare tutto il linguaggio qui, mi interessa solo elencare alcune possibilita' che offre, vorrei il vostro parere.
1) Le classi hanno attributi, metodi di classe e metodi di istanza (come in tutti i linguaggi a oggetti).
2) C'e' un runtime con capacita' di introspezione, ma non ha macchina virtuale.
3) Non esistono classi virtuali da usare come interfacce attraverso ereditarieta' multipla. Esistono "protocolli", ovvero dichiarazioni di metodi raggruppati sotto un nome che vengono implementati dalle classi. Ereditarieta' singola.
4) Le classi sono estendibili a piacere col meccanismo delle "categorie". Esempio: la classe Point non prevede il metodo "scale". Ok, lo aggiungo perche' mi serve. Non ho bisogno del codice di implementazione di Point, mi basta avere accesso alla sua dichiarazione. Una volta dichiarata la categoria in un file h e implementata nel mio codice, ecco che potro' chiamare il metodo "scale" esattamente come se fosse stato definito fin dal principio nella classe.
In altre parole, il runtime permette di aggiungere al volo metodi alle classi. Il tutto in modo veramente trasparente e semplice, se serve posso far vedere il codice.
Le categorie risolvono anche un problema tipico del C++, ovvero l'impossibilita' di estendere una classe distribuita in un framework linkato dinamicamente.
Lo conoscevate gia'? Vi ha incuriosito? Ditemi che ne pensate.
Ciao
Marco
First anonymous: (dropping any kind of nick would make delayed answers easier to read :-))
Yes, it's quite similar. With open classes, the extension methods will be added to the existing type, therefore you'll be able to apply new functions (with a member function syntax) to objects created by portions of code you can't control (libraries, components, etc). This is the main departure from inheritance-based extension. Still, you'll have strict type checking, and no violation of encapsulation as originally conceived, as the extensions methods don't have any privileged access to the private / protected part. By the same token, you have type-safe separate compilation. We also have limits, as the extension methods can't be virtual, so no chance of structural-conformance polymorphism here.
Overall, it's quite a powerful feature, but there is probably a need for some well-grounded usage guideline (analogous to Liskov's Substitution Principle for inheritance). Now, that's something worth writing (or reading :-) about...
Yes, it's quite similar. With open classes, the extension methods will be added to the existing type, therefore you'll be able to apply new functions (with a member function syntax) to objects created by portions of code you can't control (libraries, components, etc). This is the main departure from inheritance-based extension. Still, you'll have strict type checking, and no violation of encapsulation as originally conceived, as the extensions methods don't have any privileged access to the private / protected part. By the same token, you have type-safe separate compilation. We also have limits, as the extension methods can't be virtual, so no chance of structural-conformance polymorphism here.
Overall, it's quite a powerful feature, but there is probably a need for some well-grounded usage guideline (analogous to Liskov's Substitution Principle for inheritance). Now, that's something worth writing (or reading :-) about...
Anonimo #2:
Premessa: il discorso rischia di sfociare rapidamente in un dibattito semi-filosofico in cui non so se mi voglio infilare :-)).
Riguardo gran parte della tua proposta, direi che ad es. il buon Niklaus Wirth sarebbe largamente in accordo con quanto dici, dal momento che ha a lungo sostenuto che l'OOP introducesse senza necessita' nuovi concetti (come la classe) laddove esistevano da tempo alternative sperimentate (tipi "alla Pascal" e moduli "alla Modula 2").
Lasciando da parte per un istante multimethods e costruttori, va detto che a livello di potenza espressiva la proposta class-based e quella type+module-based sono fondamentalmente equivalenti, una volta che si introduca nella prima il concetto di open class o extension method che dir si voglia (che mi pare di capire non piaccia :->>, ma e' un concetto molto utile).
La differenza di impostazione resta, come accennavo sopra, un po' filosofica. Se da un lato la proposta type+module ha un grosso pregio, ovvero l'introduzione nel linguaggio di due concetti semi-ortogonali ("semi" nel senso che tu introduci il concetto di protezione anche nei tipi, e non solo nei moduli), che possono essere composti o usati singolarmente, la proposta class-based esprime direttamente nel linguaggio il concetto fondamentale del paradigma che vuole supportare.
Muovendo giusto un passo nel terreno scivoloso a cavallo tra filosofia e psicologia sperimentale :-), e' abbastanza verificato che non avere una parola nel linguaggio per esprimere direttamente un concetto porta ad un sotto-utilizzo del concetto. Piccolo esempio: in C++ non esiste la parola "interface", anche se esiste ovviamente il concetto di interfaccia (classe con solo metodi virtuali puri). Guarda caso, in C++ le interfacce vengono sotto-utilizzate rispetto alla loro reale utilita'.
Quindi, una buona ragione per avere le classi e' far si' che i programmatori le usino :-) [lo dico in tono scherzoso, ma e' una questione importante].
Salto un tot di dettagli (es. nella tua proposta non c'e' un vero "private", il private dei tuoi tipi equivale all'internal del C#, con un conseguente indebolimento dell'incapsulazione, ma sono elementi su cui si potrebbe lavorare), mentre almeno su una cosa non posso tacere :-))). I costruttori sono un concetto fondamentale perche' garantiscono che l'oggetto non sia solo un pezzo di memoria con dentro robaccia scelta da chi lo alloca, ma un elemento coerente che rispetta l'invariante della classe. Mi sfuggono invece i "problemi" che creano in C++ (salvo nel caso di ereditarieta' multipla virtuale), dove invece sono centrali nel buon uso del linguaggio (senza costruttori addio paradigma RAII), e probabilmente mi sfuggono anche quelli che creano in C# :-> (dove invece e' il modello RAD-oriented "a properties" che fa a pugni con una buona incapsulazione, e quindi pure con i costruttori...)
Premessa: il discorso rischia di sfociare rapidamente in un dibattito semi-filosofico in cui non so se mi voglio infilare :-)).
Riguardo gran parte della tua proposta, direi che ad es. il buon Niklaus Wirth sarebbe largamente in accordo con quanto dici, dal momento che ha a lungo sostenuto che l'OOP introducesse senza necessita' nuovi concetti (come la classe) laddove esistevano da tempo alternative sperimentate (tipi "alla Pascal" e moduli "alla Modula 2").
Lasciando da parte per un istante multimethods e costruttori, va detto che a livello di potenza espressiva la proposta class-based e quella type+module-based sono fondamentalmente equivalenti, una volta che si introduca nella prima il concetto di open class o extension method che dir si voglia (che mi pare di capire non piaccia :->>, ma e' un concetto molto utile).
La differenza di impostazione resta, come accennavo sopra, un po' filosofica. Se da un lato la proposta type+module ha un grosso pregio, ovvero l'introduzione nel linguaggio di due concetti semi-ortogonali ("semi" nel senso che tu introduci il concetto di protezione anche nei tipi, e non solo nei moduli), che possono essere composti o usati singolarmente, la proposta class-based esprime direttamente nel linguaggio il concetto fondamentale del paradigma che vuole supportare.
Muovendo giusto un passo nel terreno scivoloso a cavallo tra filosofia e psicologia sperimentale :-), e' abbastanza verificato che non avere una parola nel linguaggio per esprimere direttamente un concetto porta ad un sotto-utilizzo del concetto. Piccolo esempio: in C++ non esiste la parola "interface", anche se esiste ovviamente il concetto di interfaccia (classe con solo metodi virtuali puri). Guarda caso, in C++ le interfacce vengono sotto-utilizzate rispetto alla loro reale utilita'.
Quindi, una buona ragione per avere le classi e' far si' che i programmatori le usino :-) [lo dico in tono scherzoso, ma e' una questione importante].
Salto un tot di dettagli (es. nella tua proposta non c'e' un vero "private", il private dei tuoi tipi equivale all'internal del C#, con un conseguente indebolimento dell'incapsulazione, ma sono elementi su cui si potrebbe lavorare), mentre almeno su una cosa non posso tacere :-))). I costruttori sono un concetto fondamentale perche' garantiscono che l'oggetto non sia solo un pezzo di memoria con dentro robaccia scelta da chi lo alloca, ma un elemento coerente che rispetta l'invariante della classe. Mi sfuggono invece i "problemi" che creano in C++ (salvo nel caso di ereditarieta' multipla virtuale), dove invece sono centrali nel buon uso del linguaggio (senza costruttori addio paradigma RAII), e probabilmente mi sfuggono anche quelli che creano in C# :-> (dove invece e' il modello RAD-oriented "a properties" che fa a pugni con una buona incapsulazione, e quindi pure con i costruttori...)
Citrullo: [ :-))) ]
Ricordo di aver incontrato Objective C negli anni 80, leggendo il libro di Cox sulla programmazione ad oggetti. Purtroppo [dico purtroppo perche' si capisce che ti piace molto :-)] devo dire che a me non e' piaciuto granche' per alcune ragioni, tra le quali spicca la mancanza di type checking statico. Ora, invece di dilungarmi in una disamina del linguaggio che richiederebbe tantissimo tempo, ne approfitto per fare qualche riflessione di language design.
Creare un linguaggio e' molto complesso. Alcune scelte sono di dettaglio, e non influenzano in modo significativo le fondamenta del linguaggio. Ad esempio, in Objective C abbiamo i named parameters nelle chiamate ai metodi, come in Ada. Questo si puo' riportare senza traumi in altri linguaggi, perche' non tocca le fondamenta.
Altre scelte possono sembrare grandi o piccole, ma toccano le fondamenta del linguaggio. Avere una compilazione one-shot, o JIT, oppure un interprete e' ovviamente una grande scelta che plasma il linguaggio, nel bene e nel male. Avere un controllo statico sui tipi plasma il linguaggio, nuovamente nel bene e nel male. In effetti, molte cose che a te piacciono si fanno molto piu' facilmente in un linguaggio con type checking dinamico, che tuttavia ha i suoi bei problemi (per quanto mi riguarda, difficilmente accettabili). Un fattore che citi come positivo (assenza di virtual machine) ha a sua volta grossi benefici (piuttosto evidenti), ma pone ovviamente i suoi limiti (parla con un fan di Smalltalk e capirai :-), oppure pensa ad alcune cose divertenti fatte in Java o C# generando codice a run-time).
In Objective C troviamo comunque alcuni concetti di grande lungimiranza, come il concetto di Posing, che tanto mi servirebbe in C# ma unito, guarda guarda, alla possibilita' di generare codice a run-time tipica dei linguaggi JITted. In generale, Cox ha introdotto alcuni ottimi elementi per permettere al programmatore di "riprendere il controllo" di codice scritto da altri (pensando, correttamente, al programming-in-the-large), cedendo purtroppo su un terreno che non amo (il type checking dinamico, appunto, che si sposa cosi' male con il programming-in-the-large).
Quello che forse e' piu' complesso capire, e questo c'entra poco con Objective C ma lo scrivo ugualmente come spunto di riflessione, sono le piccole scelte di un linguaggio che finiscono per toccarne le fondamenta, ad esempio permettendo o meno alcuni idiomi di programmazione. Chi ha voglia di giocare un po' :-) al progettista di linguaggi, pensi ad una minuzia, come le scelte fatte per gli oggetti statici in C++ Vs. Java / C#, ed alle conseguenze reali dei diversi modelli.
Ricordo di aver incontrato Objective C negli anni 80, leggendo il libro di Cox sulla programmazione ad oggetti. Purtroppo [dico purtroppo perche' si capisce che ti piace molto :-)] devo dire che a me non e' piaciuto granche' per alcune ragioni, tra le quali spicca la mancanza di type checking statico. Ora, invece di dilungarmi in una disamina del linguaggio che richiederebbe tantissimo tempo, ne approfitto per fare qualche riflessione di language design.
Creare un linguaggio e' molto complesso. Alcune scelte sono di dettaglio, e non influenzano in modo significativo le fondamenta del linguaggio. Ad esempio, in Objective C abbiamo i named parameters nelle chiamate ai metodi, come in Ada. Questo si puo' riportare senza traumi in altri linguaggi, perche' non tocca le fondamenta.
Altre scelte possono sembrare grandi o piccole, ma toccano le fondamenta del linguaggio. Avere una compilazione one-shot, o JIT, oppure un interprete e' ovviamente una grande scelta che plasma il linguaggio, nel bene e nel male. Avere un controllo statico sui tipi plasma il linguaggio, nuovamente nel bene e nel male. In effetti, molte cose che a te piacciono si fanno molto piu' facilmente in un linguaggio con type checking dinamico, che tuttavia ha i suoi bei problemi (per quanto mi riguarda, difficilmente accettabili). Un fattore che citi come positivo (assenza di virtual machine) ha a sua volta grossi benefici (piuttosto evidenti), ma pone ovviamente i suoi limiti (parla con un fan di Smalltalk e capirai :-), oppure pensa ad alcune cose divertenti fatte in Java o C# generando codice a run-time).
In Objective C troviamo comunque alcuni concetti di grande lungimiranza, come il concetto di Posing, che tanto mi servirebbe in C# ma unito, guarda guarda, alla possibilita' di generare codice a run-time tipica dei linguaggi JITted. In generale, Cox ha introdotto alcuni ottimi elementi per permettere al programmatore di "riprendere il controllo" di codice scritto da altri (pensando, correttamente, al programming-in-the-large), cedendo purtroppo su un terreno che non amo (il type checking dinamico, appunto, che si sposa cosi' male con il programming-in-the-large).
Quello che forse e' piu' complesso capire, e questo c'entra poco con Objective C ma lo scrivo ugualmente come spunto di riflessione, sono le piccole scelte di un linguaggio che finiscono per toccarne le fondamenta, ad esempio permettendo o meno alcuni idiomi di programmazione. Chi ha voglia di giocare un po' :-) al progettista di linguaggi, pensi ad una minuzia, come le scelte fatte per gli oggetti statici in C++ Vs. Java / C#, ed alle conseguenze reali dei diversi modelli.
Grazie per la estesa risposta.
Non capisco perche' dici che Objective C manchi del controllo dei tipi statico. Come si vede qui:
http://developer.apple.com/documentation/
Cocoa/Conceptual/ObjectiveC/Articles/
chapter_4_section_8.html#//apple_ref/
doc/uid/TP30001163-CH7-BAJIFIHG
il compilatore fa il controllo statico ogni volta che il programmatore ha usato un tipo, cioe' non il generico "id" (che sarebbe il tipo del puntatore a oggetti della classe root). Se il controllo sul tipo fallisce, il compilatore produce un warning. Non puo' produrre un errore perche' il linguaggio ammette l'invio di metodi arbitrari a qualunque oggetto, ma questa mi sembra solo una finezza.
Parliamo di macchina virtuale: il suo beneficio reale e' la possibilita' di generare codice a runtime? O piuttosto la possibilita' di interrogare classi e oggetti? La via di mezzo realizzata per Objective C secondo me e' un ottimo compromesso. Una delle scelte di fondo di C++ [spero di non parlare troppo a vanvera :-)] e' quella di essere un linguaggio che permette la programmazione ad oggetti, ma allo stesso tempo non essendo un sistema ad oggetti. static_cast e dynamic_cast odorano di macro vecchissimo stile... E ci metto anche questo: gli sviluppatori C++ ora tendono ad adottare lo schema con classe root ed ereditarieta' singola, magari arricchendo la root con informazioni utili a runtime. In pratica vorrebbero il runtime di Objective C, ma non lo sanno. :-)
A proposito del programming-in-the-large, che credo sia uno dei temi piu' sentiti del tempo attuale, non e' bellissimo il modo in cui Objective C nasconde i metodi privati? Nell'interfaccia della classe si mettono i metodi pubblici, e in una o piu' categorie tenute private (cioe' semplicemente non pubblicate) vanno a finire i metodi privati. Una sorta di "privateness through obscurity". Che permette anche piu' livelli di oscurita': pubblico, strettamente privato, e magari anche mediamente privato, cioe' accessibile da altre classi del mio framework. Roba che in C++ obbliga a fare piccoli sarti mortali e porta via ore di discussione in fase di progettazione, per decidere come fare le intefacce astratte e altre amenita' del genere. E quando si vuole aggiungere un paio di metodi nuovi, una bella ricompilatona generale non la leva nessuno. Le open class, argomento originale del tuo post, sono elemento portante del programming-in-the-large.
Tocco pure l'argomento delle piccole scelte cosi' completo alla grande la mia apologia di Objective C. :-) Cio' che mi piace sopra ogni cosa e' che il codice Objective C (come quello C di cui e' un vero superset) e' "WYSIWYE" (what you see is what you execute, me lo sono inventato io). Ovvero prendi un foglio con scritto un pezzo di programma Objective C, e leggendolo capirai esattamente cosa viene chiamato e quando. La stessa cosa e' impossibile in C++ Java e C#, perche' tra costruttori, distruttori, operatori ridefiniti, insomma callback che partono quando non te lo aspetti, ci si perde in un attimo.
Cito anche il fatto che in Objective C non sono necessari i template, sempre grazie alle open class: protocolli e categorie permettono di fare tutto cio' che viene comunemente fatto in C++ sfruttando i template. Mi pare un bel guadagno.
Dai, la smetto che ne ho dette anche troppe di c... :-)
Non capisco perche' dici che Objective C manchi del controllo dei tipi statico. Come si vede qui:
http://developer.apple.com/documentation/
Cocoa/Conceptual/ObjectiveC/Articles/
chapter_4_section_8.html#//apple_ref/
doc/uid/TP30001163-CH7-BAJIFIHG
il compilatore fa il controllo statico ogni volta che il programmatore ha usato un tipo, cioe' non il generico "id" (che sarebbe il tipo del puntatore a oggetti della classe root). Se il controllo sul tipo fallisce, il compilatore produce un warning. Non puo' produrre un errore perche' il linguaggio ammette l'invio di metodi arbitrari a qualunque oggetto, ma questa mi sembra solo una finezza.
Parliamo di macchina virtuale: il suo beneficio reale e' la possibilita' di generare codice a runtime? O piuttosto la possibilita' di interrogare classi e oggetti? La via di mezzo realizzata per Objective C secondo me e' un ottimo compromesso. Una delle scelte di fondo di C++ [spero di non parlare troppo a vanvera :-)] e' quella di essere un linguaggio che permette la programmazione ad oggetti, ma allo stesso tempo non essendo un sistema ad oggetti. static_cast e dynamic_cast odorano di macro vecchissimo stile... E ci metto anche questo: gli sviluppatori C++ ora tendono ad adottare lo schema con classe root ed ereditarieta' singola, magari arricchendo la root con informazioni utili a runtime. In pratica vorrebbero il runtime di Objective C, ma non lo sanno. :-)
A proposito del programming-in-the-large, che credo sia uno dei temi piu' sentiti del tempo attuale, non e' bellissimo il modo in cui Objective C nasconde i metodi privati? Nell'interfaccia della classe si mettono i metodi pubblici, e in una o piu' categorie tenute private (cioe' semplicemente non pubblicate) vanno a finire i metodi privati. Una sorta di "privateness through obscurity". Che permette anche piu' livelli di oscurita': pubblico, strettamente privato, e magari anche mediamente privato, cioe' accessibile da altre classi del mio framework. Roba che in C++ obbliga a fare piccoli sarti mortali e porta via ore di discussione in fase di progettazione, per decidere come fare le intefacce astratte e altre amenita' del genere. E quando si vuole aggiungere un paio di metodi nuovi, una bella ricompilatona generale non la leva nessuno. Le open class, argomento originale del tuo post, sono elemento portante del programming-in-the-large.
Tocco pure l'argomento delle piccole scelte cosi' completo alla grande la mia apologia di Objective C. :-) Cio' che mi piace sopra ogni cosa e' che il codice Objective C (come quello C di cui e' un vero superset) e' "WYSIWYE" (what you see is what you execute, me lo sono inventato io). Ovvero prendi un foglio con scritto un pezzo di programma Objective C, e leggendolo capirai esattamente cosa viene chiamato e quando. La stessa cosa e' impossibile in C++ Java e C#, perche' tra costruttori, distruttori, operatori ridefiniti, insomma callback che partono quando non te lo aspetti, ci si perde in un attimo.
Cito anche il fatto che in Objective C non sono necessari i template, sempre grazie alle open class: protocolli e categorie permettono di fare tutto cio' che viene comunemente fatto in C++ sfruttando i template. Mi pare un bel guadagno.
Dai, la smetto che ne ho dette anche troppe di c... :-)
Caro Citrullo [:-))], faccio un po' fatica a risponderti perche' si vede che sei follemente innamorato :-), e nulla di buono puo' emergere dalla critica dell'amato bene.
Provo pero' a inquadrare alcune cose che dici in un contesto piu' ampio.
Objective C ha un controllo dinamico e non statico dei tipi. Quest'ultimo gli e' stato appiccicato sopra con la colla, tanto che anche nel documento che citi, in modo molto onesto dicono:
"In certain situations, it allows for compile-time type checking."
Ora, senza stare a smontare per benino tutto quanto, che richiede molto tempo, mi limito a farti notare un pezzo di codice sempre dal documento che citi:
Rectangle *aRect;
aRect = [[Shape alloc] init];
ecco che abbiamo appena mandato a quel paese ogni controllo compile-time che possa eventualmente seguire.
Il problema e' che il type checking statico e' una cosa seria :-), che va messa nelle fondamenta del linguaggio. Quando tu dici "il linguaggio ammette l'invio di metodi arbitrari a qualunque oggetto, ma questa mi sembra solo una finezza", non e' affatto una finezza, ma e' uno dei tanti punti in cui il controllo run-time, anziche' compile-time, fa capolino.
Salto invece le tue considerazioni su come i programmatori usano il C++ - mi pare che l'influenza nefasta del COM in azienda abbia messo cattivi pensieri :-). Invece mi fermo a far notare che le open classes, che come dici giustamente erano lo spunto di partenza, sono una di quelle cose molto utili ma che, per fortuna, non toccano le fondamenta del linguaggio. In C# le hanno aggiunte con facilita', con altrettanta facilita' si possono aggiungere in C++ (quasi :->, le regole di name resolution del C++ sono piu' complesse). Altri tuoi desiderata :-), come un livello di protezione analogo all'internal di C#, si aggiungerebbero con facilita' al C++ [al solito, con pro e contro; poi il friend in C++ serve proprio a questo, usato bene...]. Attualmente, il fatto di essere uno standard ANSI/ISO sta purtroppo frenando molto il C++ nell'innovazione, non solo su quanto sopra ma anche, ad es., sugli aspetti di reflection. Ed e' ovvio (per evidenza sperimentale oltre che per semplicita' dimostrativa :-)) che la reflection non ha alcun bisogno di una virtual machine, non e' per questo che esistono le virtual machine...
Salto a pie' pari anche sul WYSIWYE, perche' mi spiace infrangere i sogni degli innamorati:-)... purtroppo non appena abbiamo di mezzo il polimorfismo, e' piuttosto evidente che quello che vedi non e' quello che esegui. Se ci aggiungi il Posing di Objective C (utilissimo, come dicevo), il sogno del WYSIWYE e' ancora piu' lontano. Con un controllo run-time sui tipi, si allontana ancora. Lascio da parte cosa succederebbe con l'AOP :-)). Peraltro il tuo e' un sogno complicato, che va bilanciato con un altro aspetto cui personalmente tengo molto, ovvero la possibilita' di creare il proprio linguaggio sopra il linguaggio. Ad essere rigorosi, un WYSIWYE *reale* lo hai solo in assembly, meglio su una piattaforma senza interrupt a mezzo, e gia' che ci siamo con un processore single-core con semplice pipeline di prefetch e nient'altro (no esecuzione speculativa et similia)... utile in situazioni estremamente circoscritte ma tremendamente limitativo negli altri casi...
Salto lunghissimo anche su costruttori e distruttori. Sono un concetto talmente fondamentale per una buona OOP che devono :->> essere apprezzati, altrimenti (sorry :-))) significa che dobbiamo colmare un buco formativo.
Giusto per chiarire qualche altro aspetto: quando dici che in O.C i template non servono, ti invito a pensare a quante volte per fare cio' che realmente puoi fare in C++ con i template, in Objective C dovrai abbandonare un controllo statico sui tipi, o definire a manina protocolli molto rigidi (da questo punto di vista, il buon vecchio ML di Milner con la sua type inference era molto piu' avanti degli altri, gia' tanti anni fa; ecco, ML e' un bel linguaggio da studiare per capire meglio alcuni aspetti di language design, anche se non e' un linguaggio da usare ogni giorno per lavoro).
In generale, non vorrei che [come succede a tante persone, con linguaggi X ed Y a caso] confondessi il C++ con "il C++ usato mediamente male in un progetto enorme" ed O.C con "O.C usato bene in progetti giocattolo".
Finisco con due considerazioni provocatorie:
- come ho gia' sottolineato in altre situazioni, un aspetto centrale della cultura originale di C e C++ e' stata la critica aperta del linguaggio anche da parte di chi lo adotta. Molti hanno visto in questo una manifestazione di un cattivo linguaggio, mentre la realta' e' che si tratta di linguaggi che hanno creato una cultura di grande valore: persone che lo adottano, lo usano, lo studiano, lo estendono, ma mantengono una visione critica. Altri linguaggi (potrei fare tanti nomi e quindi non ne faccio :-) hanno creato una cultura di venerazione acritica (largamente ingiustificata :->). Per quanto mi riguarda, gia' vedere che un linguaggio sviluppa una simile cultura e' un motivo per starne lontani :-)). Detto in un altro modo: se un sostenitore del linguaggio X non sa elencarmi 10 difetti di X, c'e' qualcosa [di grosso] che non va :-))).
- Giusto per scatenare un putiferio al quale mi rifiutero' categoricamente di dare seguito :-))))), ricordiamoci anche che quando qualcosa e' realmente cosi' buono, ha successo anche nel mercato. Un subset di Objective C e' persino (da secoli) dentro gcc, e non se lo fila nessuno...
Pensaci su e mi raccomando, non prendertela :-))
Provo pero' a inquadrare alcune cose che dici in un contesto piu' ampio.
Objective C ha un controllo dinamico e non statico dei tipi. Quest'ultimo gli e' stato appiccicato sopra con la colla, tanto che anche nel documento che citi, in modo molto onesto dicono:
"In certain situations, it allows for compile-time type checking."
Ora, senza stare a smontare per benino tutto quanto, che richiede molto tempo, mi limito a farti notare un pezzo di codice sempre dal documento che citi:
Rectangle *aRect;
aRect = [[Shape alloc] init];
ecco che abbiamo appena mandato a quel paese ogni controllo compile-time che possa eventualmente seguire.
Il problema e' che il type checking statico e' una cosa seria :-), che va messa nelle fondamenta del linguaggio. Quando tu dici "il linguaggio ammette l'invio di metodi arbitrari a qualunque oggetto, ma questa mi sembra solo una finezza", non e' affatto una finezza, ma e' uno dei tanti punti in cui il controllo run-time, anziche' compile-time, fa capolino.
Salto invece le tue considerazioni su come i programmatori usano il C++ - mi pare che l'influenza nefasta del COM in azienda abbia messo cattivi pensieri :-). Invece mi fermo a far notare che le open classes, che come dici giustamente erano lo spunto di partenza, sono una di quelle cose molto utili ma che, per fortuna, non toccano le fondamenta del linguaggio. In C# le hanno aggiunte con facilita', con altrettanta facilita' si possono aggiungere in C++ (quasi :->, le regole di name resolution del C++ sono piu' complesse). Altri tuoi desiderata :-), come un livello di protezione analogo all'internal di C#, si aggiungerebbero con facilita' al C++ [al solito, con pro e contro; poi il friend in C++ serve proprio a questo, usato bene...]. Attualmente, il fatto di essere uno standard ANSI/ISO sta purtroppo frenando molto il C++ nell'innovazione, non solo su quanto sopra ma anche, ad es., sugli aspetti di reflection. Ed e' ovvio (per evidenza sperimentale oltre che per semplicita' dimostrativa :-)) che la reflection non ha alcun bisogno di una virtual machine, non e' per questo che esistono le virtual machine...
Salto a pie' pari anche sul WYSIWYE, perche' mi spiace infrangere i sogni degli innamorati:-)... purtroppo non appena abbiamo di mezzo il polimorfismo, e' piuttosto evidente che quello che vedi non e' quello che esegui. Se ci aggiungi il Posing di Objective C (utilissimo, come dicevo), il sogno del WYSIWYE e' ancora piu' lontano. Con un controllo run-time sui tipi, si allontana ancora. Lascio da parte cosa succederebbe con l'AOP :-)). Peraltro il tuo e' un sogno complicato, che va bilanciato con un altro aspetto cui personalmente tengo molto, ovvero la possibilita' di creare il proprio linguaggio sopra il linguaggio. Ad essere rigorosi, un WYSIWYE *reale* lo hai solo in assembly, meglio su una piattaforma senza interrupt a mezzo, e gia' che ci siamo con un processore single-core con semplice pipeline di prefetch e nient'altro (no esecuzione speculativa et similia)... utile in situazioni estremamente circoscritte ma tremendamente limitativo negli altri casi...
Salto lunghissimo anche su costruttori e distruttori. Sono un concetto talmente fondamentale per una buona OOP che devono :->> essere apprezzati, altrimenti (sorry :-))) significa che dobbiamo colmare un buco formativo.
Giusto per chiarire qualche altro aspetto: quando dici che in O.C i template non servono, ti invito a pensare a quante volte per fare cio' che realmente puoi fare in C++ con i template, in Objective C dovrai abbandonare un controllo statico sui tipi, o definire a manina protocolli molto rigidi (da questo punto di vista, il buon vecchio ML di Milner con la sua type inference era molto piu' avanti degli altri, gia' tanti anni fa; ecco, ML e' un bel linguaggio da studiare per capire meglio alcuni aspetti di language design, anche se non e' un linguaggio da usare ogni giorno per lavoro).
In generale, non vorrei che [come succede a tante persone, con linguaggi X ed Y a caso] confondessi il C++ con "il C++ usato mediamente male in un progetto enorme" ed O.C con "O.C usato bene in progetti giocattolo".
Finisco con due considerazioni provocatorie:
- come ho gia' sottolineato in altre situazioni, un aspetto centrale della cultura originale di C e C++ e' stata la critica aperta del linguaggio anche da parte di chi lo adotta. Molti hanno visto in questo una manifestazione di un cattivo linguaggio, mentre la realta' e' che si tratta di linguaggi che hanno creato una cultura di grande valore: persone che lo adottano, lo usano, lo studiano, lo estendono, ma mantengono una visione critica. Altri linguaggi (potrei fare tanti nomi e quindi non ne faccio :-) hanno creato una cultura di venerazione acritica (largamente ingiustificata :->). Per quanto mi riguarda, gia' vedere che un linguaggio sviluppa una simile cultura e' un motivo per starne lontani :-)). Detto in un altro modo: se un sostenitore del linguaggio X non sa elencarmi 10 difetti di X, c'e' qualcosa [di grosso] che non va :-))).
- Giusto per scatenare un putiferio al quale mi rifiutero' categoricamente di dare seguito :-))))), ricordiamoci anche che quando qualcosa e' realmente cosi' buono, ha successo anche nel mercato. Un subset di Objective C e' persino (da secoli) dentro gcc, e non se lo fila nessuno...
Pensaci su e mi raccomando, non prendertela :-))
Grazie ancora per l'estesa risposta.
Ti dico subito che se io fossi uno che se la prende te ne saresti gia' accorto. :-)
No, scherzo... solo per dire che scrivo le mie osservazioni per la curiosita' di ascoltare chi ne sa piu' di me, quindi ogni cosa che hai risposto, con sopportazione :-), e' spunto per riflettere, e non offesa personale...
Pero' non capisco perche' mi fai fare la figura del tifoso innamorato: mica ho detto che Objective C e' la chiave universale della programmazione! Con evidente autoironia, mi sono limitato a dire che l'approccio tipico di Objective C verso alcuni problemi ricorrenti mi sembra convincente, al contrario di altri linguaggi molto diffusi che su questi aspetti latitano un po'.
Trovo che le open class, pane quotidiano in Objective C, siano una possibile via per superare tali difficolta'. In C# le hanno aggiunte (perche'?) con facilita'. In C++ si possono aggiungere con pochi sforzi? Ok, quando le avremo sara' un motivo in piu' per fare il funerale di Objective C. Nel frattempo, parliamo di quello che c'e' e di cosa ci possiamo fare.
Per sgombrare il campo dal solo sospetto che il cieco innamoramento sia alla base delle mie affermazioni, daro' prova di lucidita' elencando sette difetti di Objective C:
1) Assenza di namespace.
2) Possibilita' di fare overload di metodi cambiando il tipo restituito (allucinante!).
3) Anche se il type checking dinamico non mi disgusta, quello statico dovrebbe essere piu' severo.
4) Sintassi con parentesi quadre poco naturale.
5) Le direttive @private, @protected, @public cozzano con il tipico modo di scrivere i programmi in Objective C (sara' per questo che non le implementano nel compilatore?).
6) Prestazioni: sebbene sia di solito efficiente, esistono casi specifici in cui il meccanismo di dispatching dei messaggi, ovvero le chiamate dei metodi, arranca.
7) Tendenza alla proliferazione di categorie e protocolli.
Ho pronti altri punti critici per superare abbondantemente quota dieci, ma non continuo oltre perche' fa tanto male... :-)
Mi voglio liberare anche dai cattivi pensieri aziendali che offuscherebbero la mia percezione del reale: sono veri incubi, fidati che non mi sono mai bevuto la storia del "si fa cosi'". Comunque certe pratiche sono molto diffuse in aziende grandi e piccole.
Non entro di nuovo nello specifico di Objective C [anche perche' ormai ti ho rotto il be...o :-)], mi limito a gridare al mondo che non ho nulla contro costruttori e distruttori (che esistono pure in Objective C). Francamente, non si capisce come tu abbia dedotto che non li apprezzo. Ho fatto una critica ad una scelta sintattica. Vogliamo dire WYSIWYC, con Call come ultima lettera? Va bene, c'e' il posing di mezzo, ma si parla di un approccio generale, mica di una regola di fuoco.
Piu' che io colmare il mio vuoto formativo, mi sa che tu devi colmare un vuoto negli occhiali [dai, tanto lo so che ami la polemica, e questa te la meriti :-)]: la "finezza" che mi citi non riguarda l'invio di metodi arbitrari, che non e' affatto una finezza e lo so da solo, ma il warning anziche' l'errore di compilazione.
A mio modo di vedere, l'esempio che riporti dal manuale e' da classificare come errore di programmazione, ed il compilatore dovrebbe essere piu' severo. Se uno programma male, errori analoghi li fa anche in C e C++.
E qui si apre la prateria dell'usare bene o male gli strumenti: e' colpa dello strumento troppo versatile o dell'operaio poco navigato se il risultato lascia a desiderare? Qualunque sia la risposta, l'uso sbagliato di qualunque linguaggio porta a cose brutte.
Metto un po' di entusiasmo in Objective C perche' non e' la solita minestra e, a differenza di ML, Eiffel, SmallTalk e tanti altri, funziona nel mondo reale. Quando vedo come C++, Java, C# vengono usati per risolvere problemi concreti, mi rendo conto che Objective C e' un ottimo atleta.
Sulle due provocazioni: la prima... va' la', lascia perdere che e' meglio.
La seconda e' solo una provocazione, rilancio con un'altra provocazione: facciamo finta che non siano mai esistiti tutti i linguaggi, programmi, sistemi operativi, eccetera che hanno avuto vita breve o limitato successo. Di quante grandi innovazioni ad essi dovute dovremmo privarci?
Posso essere riabilitato ora? Spero proprio di si', eh! :-)))
Ti dico subito che se io fossi uno che se la prende te ne saresti gia' accorto. :-)
No, scherzo... solo per dire che scrivo le mie osservazioni per la curiosita' di ascoltare chi ne sa piu' di me, quindi ogni cosa che hai risposto, con sopportazione :-), e' spunto per riflettere, e non offesa personale...
Pero' non capisco perche' mi fai fare la figura del tifoso innamorato: mica ho detto che Objective C e' la chiave universale della programmazione! Con evidente autoironia, mi sono limitato a dire che l'approccio tipico di Objective C verso alcuni problemi ricorrenti mi sembra convincente, al contrario di altri linguaggi molto diffusi che su questi aspetti latitano un po'.
Trovo che le open class, pane quotidiano in Objective C, siano una possibile via per superare tali difficolta'. In C# le hanno aggiunte (perche'?) con facilita'. In C++ si possono aggiungere con pochi sforzi? Ok, quando le avremo sara' un motivo in piu' per fare il funerale di Objective C. Nel frattempo, parliamo di quello che c'e' e di cosa ci possiamo fare.
Per sgombrare il campo dal solo sospetto che il cieco innamoramento sia alla base delle mie affermazioni, daro' prova di lucidita' elencando sette difetti di Objective C:
1) Assenza di namespace.
2) Possibilita' di fare overload di metodi cambiando il tipo restituito (allucinante!).
3) Anche se il type checking dinamico non mi disgusta, quello statico dovrebbe essere piu' severo.
4) Sintassi con parentesi quadre poco naturale.
5) Le direttive @private, @protected, @public cozzano con il tipico modo di scrivere i programmi in Objective C (sara' per questo che non le implementano nel compilatore?).
6) Prestazioni: sebbene sia di solito efficiente, esistono casi specifici in cui il meccanismo di dispatching dei messaggi, ovvero le chiamate dei metodi, arranca.
7) Tendenza alla proliferazione di categorie e protocolli.
Ho pronti altri punti critici per superare abbondantemente quota dieci, ma non continuo oltre perche' fa tanto male... :-)
Mi voglio liberare anche dai cattivi pensieri aziendali che offuscherebbero la mia percezione del reale: sono veri incubi, fidati che non mi sono mai bevuto la storia del "si fa cosi'". Comunque certe pratiche sono molto diffuse in aziende grandi e piccole.
Non entro di nuovo nello specifico di Objective C [anche perche' ormai ti ho rotto il be...o :-)], mi limito a gridare al mondo che non ho nulla contro costruttori e distruttori (che esistono pure in Objective C). Francamente, non si capisce come tu abbia dedotto che non li apprezzo. Ho fatto una critica ad una scelta sintattica. Vogliamo dire WYSIWYC, con Call come ultima lettera? Va bene, c'e' il posing di mezzo, ma si parla di un approccio generale, mica di una regola di fuoco.
Piu' che io colmare il mio vuoto formativo, mi sa che tu devi colmare un vuoto negli occhiali [dai, tanto lo so che ami la polemica, e questa te la meriti :-)]: la "finezza" che mi citi non riguarda l'invio di metodi arbitrari, che non e' affatto una finezza e lo so da solo, ma il warning anziche' l'errore di compilazione.
A mio modo di vedere, l'esempio che riporti dal manuale e' da classificare come errore di programmazione, ed il compilatore dovrebbe essere piu' severo. Se uno programma male, errori analoghi li fa anche in C e C++.
E qui si apre la prateria dell'usare bene o male gli strumenti: e' colpa dello strumento troppo versatile o dell'operaio poco navigato se il risultato lascia a desiderare? Qualunque sia la risposta, l'uso sbagliato di qualunque linguaggio porta a cose brutte.
Metto un po' di entusiasmo in Objective C perche' non e' la solita minestra e, a differenza di ML, Eiffel, SmallTalk e tanti altri, funziona nel mondo reale. Quando vedo come C++, Java, C# vengono usati per risolvere problemi concreti, mi rendo conto che Objective C e' un ottimo atleta.
Sulle due provocazioni: la prima... va' la', lascia perdere che e' meglio.
La seconda e' solo una provocazione, rilancio con un'altra provocazione: facciamo finta che non siano mai esistiti tutti i linguaggi, programmi, sistemi operativi, eccetera che hanno avuto vita breve o limitato successo. Di quante grandi innovazioni ad essi dovute dovremmo privarci?
Posso essere riabilitato ora? Spero proprio di si', eh! :-)))
Ho pensato a 3 risposte.
La prima cercava [un po' come le precedenti] di parlare in generale di language design, a beneficio anche di chi ha ancora voglia di leggere :-).
La seconda era un po' polemica, stile "L'arte di ottenere ragione" di Schopenhauer, e si divertiva a mettere in evidenza una certa incoerenza in alcuni passaggi.
La terza e' piu' rapida e sintetizza tutto in una sillaba: no :-)))))))))))))))
La prima cercava [un po' come le precedenti] di parlare in generale di language design, a beneficio anche di chi ha ancora voglia di leggere :-).
La seconda era un po' polemica, stile "L'arte di ottenere ragione" di Schopenhauer, e si divertiva a mettere in evidenza una certa incoerenza in alcuni passaggi.
La terza e' piu' rapida e sintetizza tutto in una sillaba: no :-)))))))))))))))
Va bene, ho capito che non c'รจ speranza. Alzo bandiera bianca e vado a studiare un po' il type checking. :-)
Ma queste benedette open class come potrebbero comparire in C++? Pensi che le vedremo davvero?
Ma queste benedette open class come potrebbero comparire in C++? Pensi che le vedremo davvero?
In C++ si potrebbero facilmente introdurre con la sintassi bruttina del C#, senza la necessita' di renderle member function statiche, ovvero con qualcosa del tipo (adattando un esempio della specifica C# 3.0):
int ToInt32( this string s )
{
// ... return something ;
}
che il compilatore potrebbe trattare come un extension method di string, grazie alla parolina magica this. Naturalmente, come in C#, gli extension method non avrebbero privilegi particolari, perche' non sono member function.
Ovviamente, a questo punto non vedo perche' non fare anche come in C# e permettere agli extension method di essere anche member function statiche e pubbliche di una classe.
Gia' anni fa qualcuno ha fatto una proposta ancora piu' radicale, ovvero che una qualunque funzione come
int ToInt32( string s )
{
// ... return something ;
}
si potesse considerare un extension method, e quindi richiamare con
string s = "123" ;
int x = s.ToInt32() ;
in fondo e' solo un rigiro sintattico, che qualcuno ha anche suggerito, in un contesto diverso, in un commento precedente a questo post.
Il fatto di essere implicitamente un extension method ha dei difetti (pensa al tuo WYSIWYE/C :->, ma in generale, pensiamo per analogia ai costruttori che sono anche degli operatori di conversione se non hanno un explicit prefisso) e dei pregi, soprattutto quando parti di codice sono gia' scritte / intoccabili.
Ci sarebbe anche un altro grosso pregio nel farlo cosi': nessuna necessita' di modifiche al parser del compilatore, solo alla parte di codice che trova la best viable function. Questa era l'idea che ho intrattenuto per un po' pensando di sperimentare col GCC ed aggiungerla, poi non ho mai avuto il tempo di farlo, anche perche' la parte di codice in questione e' piuttosto complessa (le regole per la best viable function sono complesse).
Come dicevo in un altro commento al post originale, cio' che diventa interessante ora e' non tanto lo scenario di patching/ampliamento di una classe esistente via extension method, ma secondo quali linee guida questi si inseriscono in un concetto piu' ampio di design, unendosi ai consueti strumenti dei livelli di protezione, di composizione, di ereditarieta'. Su questo ho delle idee ma non ho ancora raggiunto il Nirvana :-) e quindi devo pensarci ancora un po', e usarli anche un po' in contesti reali (in C#, direi: in C++, non credo le vedremo se non in tempi epocali, se ANSI/ISO non cambiano registro...)
int ToInt32( this string s )
{
// ... return something ;
}
che il compilatore potrebbe trattare come un extension method di string, grazie alla parolina magica this. Naturalmente, come in C#, gli extension method non avrebbero privilegi particolari, perche' non sono member function.
Ovviamente, a questo punto non vedo perche' non fare anche come in C# e permettere agli extension method di essere anche member function statiche e pubbliche di una classe.
Gia' anni fa qualcuno ha fatto una proposta ancora piu' radicale, ovvero che una qualunque funzione come
int ToInt32( string s )
{
// ... return something ;
}
si potesse considerare un extension method, e quindi richiamare con
string s = "123" ;
int x = s.ToInt32() ;
in fondo e' solo un rigiro sintattico, che qualcuno ha anche suggerito, in un contesto diverso, in un commento precedente a questo post.
Il fatto di essere implicitamente un extension method ha dei difetti (pensa al tuo WYSIWYE/C :->, ma in generale, pensiamo per analogia ai costruttori che sono anche degli operatori di conversione se non hanno un explicit prefisso) e dei pregi, soprattutto quando parti di codice sono gia' scritte / intoccabili.
Ci sarebbe anche un altro grosso pregio nel farlo cosi': nessuna necessita' di modifiche al parser del compilatore, solo alla parte di codice che trova la best viable function. Questa era l'idea che ho intrattenuto per un po' pensando di sperimentare col GCC ed aggiungerla, poi non ho mai avuto il tempo di farlo, anche perche' la parte di codice in questione e' piuttosto complessa (le regole per la best viable function sono complesse).
Come dicevo in un altro commento al post originale, cio' che diventa interessante ora e' non tanto lo scenario di patching/ampliamento di una classe esistente via extension method, ma secondo quali linee guida questi si inseriscono in un concetto piu' ampio di design, unendosi ai consueti strumenti dei livelli di protezione, di composizione, di ereditarieta'. Su questo ho delle idee ma non ho ancora raggiunto il Nirvana :-) e quindi devo pensarci ancora un po', e usarli anche un po' in contesti reali (in C#, direi: in C++, non credo le vedremo se non in tempi epocali, se ANSI/ISO non cambiano registro...)
Grazie per la tua risposta. Scusa se continuo questa discussione, visto che dici espressamente di non volerti infilare in questo dibattito, ma volevo fare delle precisazioni in risposta alle critiche che avevi mosso all'approccio che avevo delineato. Prometto che cerchero' di essere il piu' concreto possibile e di lasciare da
parte le questioni semifilosofiche. Innanzitutto spero che tu capisca la difficolta' di spiegare una cosa del genere in un post, e che non era mia intenzione "progettare" un linguaggio, ma solo di spiegare perche' l'approccio class-based mi lasciava perplesso.
Tu dici che i due approcci (class-based e type+module based) sono equivalenti come potenza espressiva. E' vero, ma a condizione che si possano definire extension methods "virtuali". Altrimenti il costrutto diventa solamente "syntactic sugar", e non altera minimamente la potenza espressiva del linguaggio. Avevo l'impressione che il dispatch dinamico sugli extension methods (in C# 3.0) non fosse consentito, pero' le specifiche non mi sembravano chiarissime su questo punto. Puoi darmi qualche chiarimento in proposito?
Non ho capito la seconda osservazione, sul fatto che i due concetti di tipo e modulo (cosi' come li avevo impostati nel mio post precedente) non siano completamente ortogonali (ci ho pensato su per un bel po'), quindi li' non posso dire niente.
Riguardo al fatto che il mio private equivale all'internal del C#: prima di tutto penso sia ovvio che nel momento in cui si rinuncia al concetto di classe la visibilita' private, come e' intesa nei linguaggi class-based, perde di significato. Come lo intendevo io, private voleva dire "visibile solo all'interno del modulo" anziche' "visibile solo all'interno del classe", che non avrebbe senso in quanto i metodi/funzioni non sono piu' definiti all'interno di un tipo.
Questo pero' non e' la stessa cosa dell'internal del C#, perche' internal vuol dire "visibile solo all'interno dell'assembly", e il concetto di modulo, come lo immaginavo io, e' una cosa molto diversa dall'assembly. Un assembly in C# e' una unita' di "deployment" mentre il modulo nella mia proposta voleva solo essere un'unita' di incapsulazione. Un assembly tipicamente contiente molte classi, e assegnarli anche la funzione esclusiva di unita' di incapsulazione porterebbe, come giustamente dici, ad un indebolimento di quest'ultima. Il modulo (come lo immaginavo io) invece dovrebbe contenere un solo tipo, o, al piu', un piccolo numero di tipi strettamente legati fra di loro (ad esempio quel genere di tipi per i quali, in un linguaggio class-based, si userebbe un costrutto come "friend"), ed un "assembly" conterrebbe un gran numero di piccoli moduli. E' invece il costrutto di classe che porta, secondo me, ad un'indebolimento dell'incapsulazione. Questo perche' spesso capita di avere metodi che, pur appartenendo logicamente ad una data classe, possono essere implementati senza accedere direttamente alla parte privata di un oggetto, ma solo appoggiandosi ad altri metodi pubblici. Con l'approccio types+functions+modules+namespaces+assemblies, questi metodi potrebbere tranquillamente essere collocati all'esterno del modulo, riducendo cosi' la quantita' di codice da esaminare per capire come funziona un dato tipo.
L'ultima tua critica riguardava i costruttori. Spiego meglio cosa avevo in mente, prima di ritrattare parzialmente quello che ho detto. Innazitutto voglio precisare che non mi e' mai passato per la testa di consentire ai clients di creare un dato oggetto impostandone arbitrariamente lo stato interno. Mi rendo perfettamente conto che consentire una cosa del genere equivale a rinunciare completamente all'incapsulazione. Quello che volevo dire e' che la funzione del costruttore puo' essere svolta (meglio) da una normale "factory function". Ad esempio, invece di scrivere
class Point
{
private double x;
private double y;
public Point(double anX, double aY)
{
x = anX;
y = aY;
}
}
si scriverebbe:
module
{
type Point
{
private double x;
private double y;
}
// Factory function for type Point
public point(double anX, double aY)
{
return new Point(x = anX, y = anY);
}
}
Ovviamente bisognerebbe comunque avere a disposizione un costruttore, visibile solo all'interno del modulo e generato automaticamente dal compilatore, che consenta di creare un oggetto e di impostarne tutti i campi in una sola operazione. La cosa richiederebbe qualche aggiustamento nel caso di classi derivate, ma non penso si tratterebbe di niente di particolarmente complicato.
I vantaggi di usare una factory function sono ben noti: le factory functions hanno un nome, consentendo, cosi' cose come
point_cartesian(double x, double y);
point_polar(double ro, double theta);
(che non si potrebbero fare con i costruttori in stile C++/Java/C#), possono restituire un oggetto di una classe derivata, e non sono costretti a creare ogni volta un oggetto diverso, ma possono restituire un oggetto gia' esistente (come si fa coi singleton). Tutti questi vantaggi portano comunque ad usare static factory functions in molti casi, quindi perche' non eliminare del tutto i costruttori?
Dopo aver letto la tua risposta, pero', mi sono reso conto che probabilmente questo approccio non funzionerebbe quando si tenta di creare un "value object" che non puo' essere copiato facilmente, come ad esempio un oggetto che incapsula una qualche risorsa. Quindi probabilmente in un linguaggio come il C++ i costruttori sono davvero necessari. Continuano comunque a sembrarmi del tutto ridondanti in linguaggi come Java (in cui gli oggetti sono manipolati solo tramite riferimento) e C# (in cui le structs non vengono mai usate per incapsulare risorse).
Un'ultima cosa e poi ti lascio in pace: mi ha incuriosito l'osservazione riguardante il modello RAD-oriented "a properties", che farebbe a pugni con una buona incapsulazione. Quando si costruisce ad esempio un'interfaccia utente, bisogna fornire un gran numero di informazioni di natura intrinsecamente dichiarativa e per lo piu' immutabili (parlo di informazioni come ad es. la posizione di un controllo sullo schermo, o il suo colore o il testo contenuto in una label). Come si fa ad incapsulare qualcosa del genere? Ed ha senso farlo? Quali sono le alternative alla soluzione property-based? Ti dispiacerebbe dilungarti un po' sull'argomento? Mi rendo conto che questa domanda e' molto vaga e confusa, ma lo e' solo perche' su questo punto molto confuso lo sono pure io.
parte le questioni semifilosofiche. Innanzitutto spero che tu capisca la difficolta' di spiegare una cosa del genere in un post, e che non era mia intenzione "progettare" un linguaggio, ma solo di spiegare perche' l'approccio class-based mi lasciava perplesso.
Tu dici che i due approcci (class-based e type+module based) sono equivalenti come potenza espressiva. E' vero, ma a condizione che si possano definire extension methods "virtuali". Altrimenti il costrutto diventa solamente "syntactic sugar", e non altera minimamente la potenza espressiva del linguaggio. Avevo l'impressione che il dispatch dinamico sugli extension methods (in C# 3.0) non fosse consentito, pero' le specifiche non mi sembravano chiarissime su questo punto. Puoi darmi qualche chiarimento in proposito?
Non ho capito la seconda osservazione, sul fatto che i due concetti di tipo e modulo (cosi' come li avevo impostati nel mio post precedente) non siano completamente ortogonali (ci ho pensato su per un bel po'), quindi li' non posso dire niente.
Riguardo al fatto che il mio private equivale all'internal del C#: prima di tutto penso sia ovvio che nel momento in cui si rinuncia al concetto di classe la visibilita' private, come e' intesa nei linguaggi class-based, perde di significato. Come lo intendevo io, private voleva dire "visibile solo all'interno del modulo" anziche' "visibile solo all'interno del classe", che non avrebbe senso in quanto i metodi/funzioni non sono piu' definiti all'interno di un tipo.
Questo pero' non e' la stessa cosa dell'internal del C#, perche' internal vuol dire "visibile solo all'interno dell'assembly", e il concetto di modulo, come lo immaginavo io, e' una cosa molto diversa dall'assembly. Un assembly in C# e' una unita' di "deployment" mentre il modulo nella mia proposta voleva solo essere un'unita' di incapsulazione. Un assembly tipicamente contiente molte classi, e assegnarli anche la funzione esclusiva di unita' di incapsulazione porterebbe, come giustamente dici, ad un indebolimento di quest'ultima. Il modulo (come lo immaginavo io) invece dovrebbe contenere un solo tipo, o, al piu', un piccolo numero di tipi strettamente legati fra di loro (ad esempio quel genere di tipi per i quali, in un linguaggio class-based, si userebbe un costrutto come "friend"), ed un "assembly" conterrebbe un gran numero di piccoli moduli. E' invece il costrutto di classe che porta, secondo me, ad un'indebolimento dell'incapsulazione. Questo perche' spesso capita di avere metodi che, pur appartenendo logicamente ad una data classe, possono essere implementati senza accedere direttamente alla parte privata di un oggetto, ma solo appoggiandosi ad altri metodi pubblici. Con l'approccio types+functions+modules+namespaces+assemblies, questi metodi potrebbere tranquillamente essere collocati all'esterno del modulo, riducendo cosi' la quantita' di codice da esaminare per capire come funziona un dato tipo.
L'ultima tua critica riguardava i costruttori. Spiego meglio cosa avevo in mente, prima di ritrattare parzialmente quello che ho detto. Innazitutto voglio precisare che non mi e' mai passato per la testa di consentire ai clients di creare un dato oggetto impostandone arbitrariamente lo stato interno. Mi rendo perfettamente conto che consentire una cosa del genere equivale a rinunciare completamente all'incapsulazione. Quello che volevo dire e' che la funzione del costruttore puo' essere svolta (meglio) da una normale "factory function". Ad esempio, invece di scrivere
class Point
{
private double x;
private double y;
public Point(double anX, double aY)
{
x = anX;
y = aY;
}
}
si scriverebbe:
module
{
type Point
{
private double x;
private double y;
}
// Factory function for type Point
public point(double anX, double aY)
{
return new Point(x = anX, y = anY);
}
}
Ovviamente bisognerebbe comunque avere a disposizione un costruttore, visibile solo all'interno del modulo e generato automaticamente dal compilatore, che consenta di creare un oggetto e di impostarne tutti i campi in una sola operazione. La cosa richiederebbe qualche aggiustamento nel caso di classi derivate, ma non penso si tratterebbe di niente di particolarmente complicato.
I vantaggi di usare una factory function sono ben noti: le factory functions hanno un nome, consentendo, cosi' cose come
point_cartesian(double x, double y);
point_polar(double ro, double theta);
(che non si potrebbero fare con i costruttori in stile C++/Java/C#), possono restituire un oggetto di una classe derivata, e non sono costretti a creare ogni volta un oggetto diverso, ma possono restituire un oggetto gia' esistente (come si fa coi singleton). Tutti questi vantaggi portano comunque ad usare static factory functions in molti casi, quindi perche' non eliminare del tutto i costruttori?
Dopo aver letto la tua risposta, pero', mi sono reso conto che probabilmente questo approccio non funzionerebbe quando si tenta di creare un "value object" che non puo' essere copiato facilmente, come ad esempio un oggetto che incapsula una qualche risorsa. Quindi probabilmente in un linguaggio come il C++ i costruttori sono davvero necessari. Continuano comunque a sembrarmi del tutto ridondanti in linguaggi come Java (in cui gli oggetti sono manipolati solo tramite riferimento) e C# (in cui le structs non vengono mai usate per incapsulare risorse).
Un'ultima cosa e poi ti lascio in pace: mi ha incuriosito l'osservazione riguardante il modello RAD-oriented "a properties", che farebbe a pugni con una buona incapsulazione. Quando si costruisce ad esempio un'interfaccia utente, bisogna fornire un gran numero di informazioni di natura intrinsecamente dichiarativa e per lo piu' immutabili (parlo di informazioni come ad es. la posizione di un controllo sullo schermo, o il suo colore o il testo contenuto in una label). Come si fa ad incapsulare qualcosa del genere? Ed ha senso farlo? Quali sono le alternative alla soluzione property-based? Ti dispiacerebbe dilungarti un po' sull'argomento? Mi rendo conto che questa domanda e' molto vaga e confusa, ma lo e' solo perche' su questo punto molto confuso lo sono pure io.
Capisco bene le difficolta' di affrontare un tema molto articolato in un blog. E' uno dei motivi per cui mi limito, di solito, a dire qualcosa che possa vagamente ispirare qualche riflessione.
Provo a rispondere ad alcune delle tue osservazioni, ed al contempo provo a ritornare anche alla tua domanda originale, "che vantaggi ci sono ad usare le classi".
Iniziamo con gli extension method virtuali. Come hai correttamente intuito, in C# gli extension method non possono, attualmente, essere virtuali. C'e' un'ottima ragione pratica per questo. Il dispatch dinamico dei metodi, nella gran parte dei linguaggi OO *efficienti*, passa attraverso un concetto di VTable. La VTable viene composta a compile time, ed e' necessario costruire la VTable prima di compilare il codice che effettua chiamate ai metodi virtuali, in modo tale da rendere tale codice efficiente (in pratica, index-based, con indice intero). Questa e' peraltro la ragione per cui le partial classes (da non confondere con le open classes / extension methods) del C# richiedono che tutte le parti di una classe siano note al momento della compilazione dell'unico assembly che conterra' la classe.
Ora, per essere realmente utili, gli extension method devono poter essere aggiunti a posteriori, anche per classi di cui non abbiamo i sorgenti, quindi in altri assembly. Se gli extension methods potessero essere virtuali, dovrebbero estendere la VTable originale della classe. Sin qui puo' sembrare semplice: i nuovi metodi ricevono un nuovo indice, che viene usato nelle chiamate. In realta' non funziona, perche' possiamo avere due nuovi assembly, compilati in modo indipendente, ognuno dei quali aggiunge un extension method virtuale, che rischia di avere lo stesso indice.
Non sono problemi insormontabili, soprattutto in un linguaggio JITted, ma per riassumere diciamo che e' complesso ottenere un meccanismo di metodi virtuali che sia:
- molto efficiente
- aperto alle estensioni
- supporti la compilazione separata
Spero sia ovvio come un problema analogo si presenti anche con la tua proposta. Anzi, ad essere sinceri, nel tuo caso raggiungere un dispatch efficiente e' molto piu' complesso. La classe (eccola li' che salta fuori ;-) e' un posto naturale in cui avere una VTable con indice singolo (intero). Se rinunciamo alle classi, diventa piu' complesso avere un dispatch efficiente (questa, che non avevo citato nella scorsa occasione, e' quindi un'altra ottima ragione per avere le classi). I linguaggi che adottano uno schema non class-based devono fare ricorso a tecniche molto sofisticate per essere ragiovolmente efficienti nel dispatch dinamico, tecnice generalmente possibili solo attraverso una compilazione just in time. Per qualche idea, chi e' interessato puo' far riferimento ad un mio (vecchio ma ancora attuale :-) articolo, "Self, un moderno linguaggio object oriented ed il suo compilatore", che spiega un po' di problemi e tecniche. Morale della storia: se con un approccio come il tuo supportiamo i metodi virtuali in modo efficiente e con compilazione separata, con la stessa fatica supportiamo anche gli extension method virtuali, efficienti, con compilazione separata. Non ci sono problemi concettuali, la potenza espressiva dei formalismi e' identica, le difficolta' pratiche addirittura minori avendo le classi.
Arriviamo all'ortogonalita'. Mi pare piuttosto semplice (?). Il tuo "tipo" e' anche un meccanismo di incapsulazione: dice cosa e' visibile al solo modulo che lo contiene (private) e cosa e' visibile negli altri moduli (public). Il tuo "modulo" e' a sua volta un meccanismo di incapsulazione (anche se opera su tipi e funzioni anziche' su dati). Quindi non sono concetti totalmente ortogonali.
Ovviamente si puo' rimediare in diversi modi, che non mi metto ad enumerare (es. il tipo e' solo private, il modulo fornisce eventuali accessors, ecc). Tieni pero' presente che cosi' come e' strutturato nel tuo primo post, a livello semantico (non sintattico) c'e' un fastidioso accoppiamento tra modulo e tipo. Il modulo conosce ovviamente il tipo, visto che lo contiene. Il tipo conosce il modulo, visto che decide cosa si vede nel modulo che lo contiene e cosa si vede negli altri moduli (ripeto, e' una dipendenza semantica, non sintattica). A questo punto, penso non si faccia fatica a capire come mai piu' di un pensatore abbia detto: abbiamo due concetti con dipendenze mutue, non ortogonali, entrambi peraltro simili sotto altri aspetti (sono tutti e due container di altre cose). Fondiamoli in uno solo! Ed ecco la classe.
Sarebbe invece interessante (forse) cercare altre ortogonalita' piu' forti (raggruppamenti chiusi o aperti, con nome o senza, per sostituire namespace [anonimi], classi chiuse, classi aperte). Ma non e' che senta un fortissimo richiamo in quel senso: l'ortogonalita' e' un bel concetto di design, sin quando non impoverisce il linguaggio sull'altare dell'astrazione. Nella pratica, gia' mi vedo folle di programmatori che dato un linguaggio come il tuo lo usano come il Pascal, buttando alle ortiche gli oggetti.
Ben fondata invece la distinzione che fai tra assembly e modulo. Per quanto, in verita', la cosa sia piu' complessa (vedi magari il mio UML Manuale di Stile sul fatto che il Component [ = assembly ] e' una unita' concettuale e non fisica, tanto che pure in .NET abbiamo assembly logici costituiti da piu' moduli "fisici" (binari) [gli "assembly multimodulo"]). Sicuramente, il concetto di assembly in .NET e' ampiamente migliorabile :-).
Tornando a classi VS. tipi + moduli, quando dici:
"Il modulo (come lo immaginavo io) invece dovrebbe contenere un solo tipo, o, al piu', un piccolo numero di tipi [...]
molti pensatori direbbero: se un modulo di norma contiene un solo tipo, sono concetti ridondanti e vanno fusi. Riecco la classe. Per i rari casi in cui metteresti un piccolo numero di tipi nel modulo, ecco il friend o l'internal...
Sull'incapsulazione debole delle classi, era proprio l'idea di Meyers, e l'idea mia che con gli extension method possiamo aumentare l'incapsulazione by design [con qualche rischio downstream]. Prima o poi ci scrivo su qualcosina.
Vediamo se sopravvivo :-))) sino ai costruttori. La tua proposta, come dici, non elimina totalmente i costruttori, ne introduce comunque uno privato e sintetizzato dal compilatore. Questo purtroppo e' un po' problematico. Non solo per le questioni di efficienza che gia' ti sono venute in mente, ma anche perche' e' un po' limitativo pensare che il costruttore sia sempre sintetizzabile: pensa a tutte le situazioni in cui il compilatore C++ non puo sintetizzare un costruttore di default o di copia (il tuo, in pratica, e' un costruttore di copia sintetizzato, in cui il parametro e' disaggregato nei singoli membri). Al solito, in linguaggi con oggetti solo reference-based la cosa e' piu' semplice, ma la mia segreta speranza e' che gradualmente tutti si rendano conto di quanto questi linguaggi siano sbagliati :-)).
Mi fermo, avrei altro da dire su costruttori Vs factories, su cosa sarebbe utile (e fattibile) in un'ottica di linguaggio a due livelli, ecc, ma diventa davvero troppo lungo. Peraltro, molto pragmaticamente puoi gia' ottenere quello che desideri con molti linguaggi OO: metti un costruttore privato e dei metodi di costruzione statici. Di nuovo, temo che ci si trovi piu' sul piano filosofico :-) che su quello della potenza espressiva.
Sulle properties, sara' per un'altra volta, ma almeno una cosa potrei dirla: l'evidente utilita' (in certe situazioni) di un idioma non dovrebbe prevalere sul paradigma. Quando in C# 3.0 mi mettono una sintassi di costruzione terrificante :-)) come gli object initializers, significa che stiamo perdendo per strada alcuni concetti molto importanti...
Provo a rispondere ad alcune delle tue osservazioni, ed al contempo provo a ritornare anche alla tua domanda originale, "che vantaggi ci sono ad usare le classi".
Iniziamo con gli extension method virtuali. Come hai correttamente intuito, in C# gli extension method non possono, attualmente, essere virtuali. C'e' un'ottima ragione pratica per questo. Il dispatch dinamico dei metodi, nella gran parte dei linguaggi OO *efficienti*, passa attraverso un concetto di VTable. La VTable viene composta a compile time, ed e' necessario costruire la VTable prima di compilare il codice che effettua chiamate ai metodi virtuali, in modo tale da rendere tale codice efficiente (in pratica, index-based, con indice intero). Questa e' peraltro la ragione per cui le partial classes (da non confondere con le open classes / extension methods) del C# richiedono che tutte le parti di una classe siano note al momento della compilazione dell'unico assembly che conterra' la classe.
Ora, per essere realmente utili, gli extension method devono poter essere aggiunti a posteriori, anche per classi di cui non abbiamo i sorgenti, quindi in altri assembly. Se gli extension methods potessero essere virtuali, dovrebbero estendere la VTable originale della classe. Sin qui puo' sembrare semplice: i nuovi metodi ricevono un nuovo indice, che viene usato nelle chiamate. In realta' non funziona, perche' possiamo avere due nuovi assembly, compilati in modo indipendente, ognuno dei quali aggiunge un extension method virtuale, che rischia di avere lo stesso indice.
Non sono problemi insormontabili, soprattutto in un linguaggio JITted, ma per riassumere diciamo che e' complesso ottenere un meccanismo di metodi virtuali che sia:
- molto efficiente
- aperto alle estensioni
- supporti la compilazione separata
Spero sia ovvio come un problema analogo si presenti anche con la tua proposta. Anzi, ad essere sinceri, nel tuo caso raggiungere un dispatch efficiente e' molto piu' complesso. La classe (eccola li' che salta fuori ;-) e' un posto naturale in cui avere una VTable con indice singolo (intero). Se rinunciamo alle classi, diventa piu' complesso avere un dispatch efficiente (questa, che non avevo citato nella scorsa occasione, e' quindi un'altra ottima ragione per avere le classi). I linguaggi che adottano uno schema non class-based devono fare ricorso a tecniche molto sofisticate per essere ragiovolmente efficienti nel dispatch dinamico, tecnice generalmente possibili solo attraverso una compilazione just in time. Per qualche idea, chi e' interessato puo' far riferimento ad un mio (vecchio ma ancora attuale :-) articolo, "Self, un moderno linguaggio object oriented ed il suo compilatore", che spiega un po' di problemi e tecniche. Morale della storia: se con un approccio come il tuo supportiamo i metodi virtuali in modo efficiente e con compilazione separata, con la stessa fatica supportiamo anche gli extension method virtuali, efficienti, con compilazione separata. Non ci sono problemi concettuali, la potenza espressiva dei formalismi e' identica, le difficolta' pratiche addirittura minori avendo le classi.
Arriviamo all'ortogonalita'. Mi pare piuttosto semplice (?). Il tuo "tipo" e' anche un meccanismo di incapsulazione: dice cosa e' visibile al solo modulo che lo contiene (private) e cosa e' visibile negli altri moduli (public). Il tuo "modulo" e' a sua volta un meccanismo di incapsulazione (anche se opera su tipi e funzioni anziche' su dati). Quindi non sono concetti totalmente ortogonali.
Ovviamente si puo' rimediare in diversi modi, che non mi metto ad enumerare (es. il tipo e' solo private, il modulo fornisce eventuali accessors, ecc). Tieni pero' presente che cosi' come e' strutturato nel tuo primo post, a livello semantico (non sintattico) c'e' un fastidioso accoppiamento tra modulo e tipo. Il modulo conosce ovviamente il tipo, visto che lo contiene. Il tipo conosce il modulo, visto che decide cosa si vede nel modulo che lo contiene e cosa si vede negli altri moduli (ripeto, e' una dipendenza semantica, non sintattica). A questo punto, penso non si faccia fatica a capire come mai piu' di un pensatore abbia detto: abbiamo due concetti con dipendenze mutue, non ortogonali, entrambi peraltro simili sotto altri aspetti (sono tutti e due container di altre cose). Fondiamoli in uno solo! Ed ecco la classe.
Sarebbe invece interessante (forse) cercare altre ortogonalita' piu' forti (raggruppamenti chiusi o aperti, con nome o senza, per sostituire namespace [anonimi], classi chiuse, classi aperte). Ma non e' che senta un fortissimo richiamo in quel senso: l'ortogonalita' e' un bel concetto di design, sin quando non impoverisce il linguaggio sull'altare dell'astrazione. Nella pratica, gia' mi vedo folle di programmatori che dato un linguaggio come il tuo lo usano come il Pascal, buttando alle ortiche gli oggetti.
Ben fondata invece la distinzione che fai tra assembly e modulo. Per quanto, in verita', la cosa sia piu' complessa (vedi magari il mio UML Manuale di Stile sul fatto che il Component [ = assembly ] e' una unita' concettuale e non fisica, tanto che pure in .NET abbiamo assembly logici costituiti da piu' moduli "fisici" (binari) [gli "assembly multimodulo"]). Sicuramente, il concetto di assembly in .NET e' ampiamente migliorabile :-).
Tornando a classi VS. tipi + moduli, quando dici:
"Il modulo (come lo immaginavo io) invece dovrebbe contenere un solo tipo, o, al piu', un piccolo numero di tipi [...]
molti pensatori direbbero: se un modulo di norma contiene un solo tipo, sono concetti ridondanti e vanno fusi. Riecco la classe. Per i rari casi in cui metteresti un piccolo numero di tipi nel modulo, ecco il friend o l'internal...
Sull'incapsulazione debole delle classi, era proprio l'idea di Meyers, e l'idea mia che con gli extension method possiamo aumentare l'incapsulazione by design [con qualche rischio downstream]. Prima o poi ci scrivo su qualcosina.
Vediamo se sopravvivo :-))) sino ai costruttori. La tua proposta, come dici, non elimina totalmente i costruttori, ne introduce comunque uno privato e sintetizzato dal compilatore. Questo purtroppo e' un po' problematico. Non solo per le questioni di efficienza che gia' ti sono venute in mente, ma anche perche' e' un po' limitativo pensare che il costruttore sia sempre sintetizzabile: pensa a tutte le situazioni in cui il compilatore C++ non puo sintetizzare un costruttore di default o di copia (il tuo, in pratica, e' un costruttore di copia sintetizzato, in cui il parametro e' disaggregato nei singoli membri). Al solito, in linguaggi con oggetti solo reference-based la cosa e' piu' semplice, ma la mia segreta speranza e' che gradualmente tutti si rendano conto di quanto questi linguaggi siano sbagliati :-)).
Mi fermo, avrei altro da dire su costruttori Vs factories, su cosa sarebbe utile (e fattibile) in un'ottica di linguaggio a due livelli, ecc, ma diventa davvero troppo lungo. Peraltro, molto pragmaticamente puoi gia' ottenere quello che desideri con molti linguaggi OO: metti un costruttore privato e dei metodi di costruzione statici. Di nuovo, temo che ci si trovi piu' sul piano filosofico :-) che su quello della potenza espressiva.
Sulle properties, sara' per un'altra volta, ma almeno una cosa potrei dirla: l'evidente utilita' (in certe situazioni) di un idioma non dovrebbe prevalere sul paradigma. Quando in C# 3.0 mi mettono una sintassi di costruzione terrificante :-)) come gli object initializers, significa che stiamo perdendo per strada alcuni concetti molto importanti...
Questa discussione e' troppo interessante per terminare! Potresti riprendere questi argomenti in futuro?
La mia sensazione e' che, come forse sta emergendo dai commenti precedenti, la possibilita' di estendere le classi esistenti sia centrale nel dibattito sui linguaggi. E' molto piu' importante di altre cose perche' ha un impatto diretto sulle tecniche di modellazione delle classi. In gioco non c'e' solo la comodita' di aggiungere un paio di metodi quando servono.
Immaginiamo di dover progettare da zero una libreria che assolva a determinati compiti. Qualunque sia il metodo che seguiremo per modellare le classi, esso dovra' tener presente le caratteristiche del linguaggio che useremo per l'implementazione. Il vincolo di NON estendibilita' delle classi inevitabilmente si fara' sentire, se c'e'. Ognuno puo' focalizzarsi sugli esempi concreti di librerie reali che conosce, ce ne sono per tutti i gusti.
Per quello che ho potuto vedere io, c'e' un'impronta di base che distingue i due scenari. Quando c'e' il vincolo, si tende ad avere un grande numero di classi, molto ben definite e interconnesse. L'evoluzione di un tale modello di classi e' legata alle scelte che si sono fatte nei primi momenti. Non e' raro (sempre secondo la mia personale esperienza) trovare modelli "sovradimensionati" rispetto alle esigenze del momento, perche' si vogliono tenere aperte quante piu' strade future possibili.
Nell'altro caso, quando non siamo vincolati, le classi sono in numero inferiore e sono meno interconnesse.
Con tutta l'imprecisione e semplificazione brutale che posso usare pur di far passare un concetto, nel primo caso mi aspetto di trovare, tra le altre, classi che modellano azioni. Nel secondo caso mi aspetto di trovare quasi soltanto classi che modellano oggetti. Le azioni sono lasciate ai metodi (come e' naturale che sia?, mi chiedo).
Queste diverse impronte non sono sempre ben marcate, certamente dipende da cosa deve fare la libreria, e credo che dipenda anche dall'abitudine a ragionare piu' nei termini del primo caso piuttosto che del secondo. Vorrei sapere cosa ne dite.
Come esempio ideale ho in mente i framework dedicati all'interfaccia utente. Da un lato metto cose tipo i vari framework Microsoft, oppure PowerPlant di Metrowerks, che andava forte fino a qualche anno fa. In generale sono modelli abbastanza aggrovigliati che presentano una certa difficolta' di estensione.
Dall'altro lato metto Cocoa, il framework attualmente in Mac OS X e realizzato in Objective C, concepito quindi per sfruttare l'estendibilita' delle classi. Il suo modello e' molto piu' compatto, e si vede chiaramente che ogni classe rappresenta oggetti reali, non concetti o azioni. Ci sono molti piu' metodi? Non mi pare. Di sicuro i metodi hanno un ruolo piu' importante.
Dal punto di vista del programmatore cliente di tali framework, preferisco un modello piu' compatto. Poche idee, fondate su concetti che non muteranno nel tempo, e possibilita' di aggiungere nuovi comportamenti sotto forma di metodi. Non so se questo pensiero e' simile al Nirvana di Carlo Pescio... :-)
Comunque vada, penso che potremmo vedere grandi cambiamenti dovuti alle open class.
Una parola soltanto su moduli, assembly, eccetera. Non e' troppo complicato come sistema? Ce n'e' veramente bisogno?
La mia sensazione e' che, come forse sta emergendo dai commenti precedenti, la possibilita' di estendere le classi esistenti sia centrale nel dibattito sui linguaggi. E' molto piu' importante di altre cose perche' ha un impatto diretto sulle tecniche di modellazione delle classi. In gioco non c'e' solo la comodita' di aggiungere un paio di metodi quando servono.
Immaginiamo di dover progettare da zero una libreria che assolva a determinati compiti. Qualunque sia il metodo che seguiremo per modellare le classi, esso dovra' tener presente le caratteristiche del linguaggio che useremo per l'implementazione. Il vincolo di NON estendibilita' delle classi inevitabilmente si fara' sentire, se c'e'. Ognuno puo' focalizzarsi sugli esempi concreti di librerie reali che conosce, ce ne sono per tutti i gusti.
Per quello che ho potuto vedere io, c'e' un'impronta di base che distingue i due scenari. Quando c'e' il vincolo, si tende ad avere un grande numero di classi, molto ben definite e interconnesse. L'evoluzione di un tale modello di classi e' legata alle scelte che si sono fatte nei primi momenti. Non e' raro (sempre secondo la mia personale esperienza) trovare modelli "sovradimensionati" rispetto alle esigenze del momento, perche' si vogliono tenere aperte quante piu' strade future possibili.
Nell'altro caso, quando non siamo vincolati, le classi sono in numero inferiore e sono meno interconnesse.
Con tutta l'imprecisione e semplificazione brutale che posso usare pur di far passare un concetto, nel primo caso mi aspetto di trovare, tra le altre, classi che modellano azioni. Nel secondo caso mi aspetto di trovare quasi soltanto classi che modellano oggetti. Le azioni sono lasciate ai metodi (come e' naturale che sia?, mi chiedo).
Queste diverse impronte non sono sempre ben marcate, certamente dipende da cosa deve fare la libreria, e credo che dipenda anche dall'abitudine a ragionare piu' nei termini del primo caso piuttosto che del secondo. Vorrei sapere cosa ne dite.
Come esempio ideale ho in mente i framework dedicati all'interfaccia utente. Da un lato metto cose tipo i vari framework Microsoft, oppure PowerPlant di Metrowerks, che andava forte fino a qualche anno fa. In generale sono modelli abbastanza aggrovigliati che presentano una certa difficolta' di estensione.
Dall'altro lato metto Cocoa, il framework attualmente in Mac OS X e realizzato in Objective C, concepito quindi per sfruttare l'estendibilita' delle classi. Il suo modello e' molto piu' compatto, e si vede chiaramente che ogni classe rappresenta oggetti reali, non concetti o azioni. Ci sono molti piu' metodi? Non mi pare. Di sicuro i metodi hanno un ruolo piu' importante.
Dal punto di vista del programmatore cliente di tali framework, preferisco un modello piu' compatto. Poche idee, fondate su concetti che non muteranno nel tempo, e possibilita' di aggiungere nuovi comportamenti sotto forma di metodi. Non so se questo pensiero e' simile al Nirvana di Carlo Pescio... :-)
Comunque vada, penso che potremmo vedere grandi cambiamenti dovuti alle open class.
Una parola soltanto su moduli, assembly, eccetera. Non e' troppo complicato come sistema? Ce n'e' veramente bisogno?
Citrullo: sono contento che la discussione sia interessante, pero' per adesso preferirei chiuderla qui.
Le tematiche di language design (e le loro strette correlazioni con il library design e l'application design) sono comunque un argomento di mio grande interesse, per cui ci ritornero' sicuramente in futuro, magari prendendo anche qualche spunto da cio' che dicevi.
Su assembly e compagni, c'e' sicuramente della complessita' a mio avviso inutile (e per altri versi, limitazioni fastidiose). Ma questo e' un argomento meno interessante :-))).
Post a Comment
Le tematiche di language design (e le loro strette correlazioni con il library design e l'application design) sono comunque un argomento di mio grande interesse, per cui ci ritornero' sicuramente in futuro, magari prendendo anche qualche spunto da cio' che dicevi.
Su assembly e compagni, c'e' sicuramente della complessita' a mio avviso inutile (e per altri versi, limitazioni fastidiose). Ma questo e' un argomento meno interessante :-))).
<< Home





