A livello di implementazione, ciò che descrivi è all'incirca quello che succede nei linguaggi OOP. la risposta di amon va in questo in modo più dettagliato. Se non ti interessa dei tipi, allora è fondamentalmente la storia completa. Questo fa parte del motivo per cui ci sono cento e un sistema di oggetti in Scheme, ed è facile eseguire il roll-out in linguaggi tipizzati dinamicamente che hanno alcune nozioni di funzioni di prima classe o anche di puntatori di funzioni di seconda classe.
Quando prendi in considerazione il controllo dei tipi, le cose diventano un po 'più complicate e cominciano a comparire distinzioni abbastanza forti. Come semplice esempio, diciamo che hai una classe con alcune variabili di istanza "private". Se lo fai in una struct in C, dove intendi inserire le variabili private? Se li metti nella struct, allora non sono veramente privati e non puoi passare qualche altra struct con variabili private diverse ma la stessa interfaccia pubblica a funzioni che accettano il tuo tipo di struct. L'alternativa è avere un void *
che punta allo stato privato dell'oggetto. Questo è brutto, ma il meglio che puoi fare e ciò che è effettivamente fatto in C. Quello che in realtà vorrai fare è usare un tipo esistenziale. Qualcosa come:
struct Foo<S> { // For efficiency reason's, you'd want an approach like amon's
S *internalState;
int A, B, C, D;
int (*GetItemSum)(Foo<S> *this);
}
typedef (exists S.Foo<S>) Foo; // Roughly, Java's Foo<?>
Se la tua lingua ha funzioni di ordine superiore, allora questo esistenziale è già associato alla nozione di tipo di funzione, e possiamo evitare anche la bruttezza del campo internalState
. Possiamo solo fare:
struct Foo {
int A, B, C, D;
Func<Foo *, int> *GetItemSum;
}
Ma questo ha ancora un problema. Se creo una struct Bar
che soddisfi la stessa interfaccia ma forse abbia qualcosa in più, una situazione simile all'ereditarietà, GetItemSum
deve ancora essere una Foo
accepting function, non un Bar
che accetta uno. In realtà, quello che vorremmo dire è che il primo parametro di GetItemSum
deve corrispondere al tipo di struct che lo contiene. Il costrutto che fa questo è chiamato self-types. Una forma semplice di esso è essenzialmente incorporata nei linguaggi OO e il termine di solito si riferisce a forme più elaborate. Ci sono un paio di modi per evitare questo problema. Innanzitutto, potremmo spostare i metodi dalla struct come nel codice della domanda originale, ma questo sposta semplicemente il problema e lo rende così che non è possibile sovrascrivere i metodi. In alternativa, potremmo semplicemente avere GetItemSum
vicino al parametro this
, ovvero avere Foo
essere:
struct Foo {
int A, B, C, D;
Func<int> *GetItemSum;
}
Questo funziona finché non si desidera eseguire la sottoclasse, ovvero l'ereditarietà dell'implementazione. Se Foo
aveva un altro metodo, ad esempio PreProcess
, che era stato invocato da GetItemSum
, questo approccio avrebbe portato al seguente problema. Se ho creato una sottoclasse Bar
che ha superato PreProcess
, che è stata sostituita con la propria copia, ma altrimenti delegata alle implementazioni di Foo
, il metodo GetItemSum
non chiamerebbe Bar
's PreProcess
ma Foo
. Questo è il motivo per cui la "ricorsione" aperta si presenta spesso quando si parla di lingue OO. L'ultima codifica, in cui GetItemSum
non richiede parametri, richiede al "costruttore" di definire ricorsivamente Foo
in termini di se stesso. Per consentire ai metodi di chiamare metodi che potrebbero essere sovrascritti in una sottoclasse, questa ricorsione deve rimanere aperta.
Ad ogni modo, le codifiche di conservazione del tipo di programmazione orientata agli oggetti in, tipicamente, i calcoli lambda calcolati sono stati studiati abbastanza pesantemente negli anni '90. La maggior parte della complessità è stata causata da cose come sottoclassi e sovrascrittura del metodo. Le versioni complete di queste codifiche erano in genere troppo pesanti per essere utilizzate seriamente. Le codifiche (anche alcune di quelle più semplici) illustrano quanta complessità è nascosta dietro le nozioni "di base" di OO.
È normale che la gente parli di come sia "facile" codificare OO in, per esempio, linguaggi funzionali, ma tendono a o) a ignorare i tipi, oppure b) utilizzare una nozione molto semplificata di OO. La buona notizia, tuttavia, è che molte delle funzionalità di codifica più difficili sono oggi meno popolari, come l'ereditarietà dell'implementazione. Vi sono anche dei vantaggi nel suddividere le nozioni OO in concetti più semplici poiché alcuni problemi, come il problema del metodo binario, sono più facili da gestire data la flessibilità extra di avere i pezzi che costituiscono la nozione di "classe".