I due concetti sono molto simili. Nei normali linguaggi OOP, colleghiamo un vtable (o per le interfacce: itable) a ciascun oggetto:
| this
v
+---+---+---+
| V | a | b | the object with fields a, b
+---+---+---+
|
v
+---+---+---+
| o | p | q | the vtable with method slots o(), p(), q()
+---+---+---+
Questo ci permette di invocare metodi simili a this->vtable.p(this)
.
In Haskell, la tabella dei metodi è più simile a un argomento nascosto implicito:
method :: Class a => a -> a -> Int
assomiglierebbe alla funzione C ++
template<typename A>
int method(Class<A>*, A*, A*)
dove Class<A>
è un'istanza di typeclass Class
per type A
. Un metodo sarebbe invocato come
typeclass_instance->p(value_ptr);
L'istanza è separata dai valori. I valori mantengono ancora il loro tipo effettivo. Mentre i typeclass permettono un certo polimorfismo, questo non è il polimorfismo di sottotipizzazione. Ciò rende impossibile creare un elenco di valori che soddisfano un Class
. Per esempio. supponendo che abbiamo instance Class Int ...
e instance Class String ...
, non possiamo creare un tipo di lista eterogeneo come [Class]
che ha valori come [42, "foo"]
. (Ciò è possibile quando si utilizza l'estensione "tipi esistenziali", che passa in effetti all'approccio Vai).
In Go, un valore non implementa un set fisso di interfacce. Di conseguenza non può avere un puntatore vtable. Invece, i puntatori ai tipi di interfaccia sono implementati come indicatori di grasso che includono un puntatore ai dati, un altro puntatore all'endable:
'this' fat pointer
+---+---+
| | |
+---+---+
____/ \_________
v v
+---+---+---+ +---+---+
| o | p | q | | a | b | the data with
+---+---+---+ +---+---+ fields a, b
itable with method
slots o(), p(), q()
this.itable->p(this.data_ptr)
L'itable è combinato con i dati in un puntatore grasso quando si esegue il cast da un valore normale a un tipo di interfaccia. Una volta che hai un tipo di interfaccia, il tipo effettivo di dati è diventato irrilevante. Infatti, non è possibile accedere direttamente ai campi senza passare attraverso i metodi o downcasting dell'interfaccia (che potrebbe non riuscire).
L'approccio di Go alla distribuzione dell'interfaccia ha un costo: ogni puntatore polimorfico è due volte più grande di un normale puntatore. Inoltre, la trasmissione da un'interfaccia all'altra comporta la copia dei puntatori del metodo su un nuovo vtable. Ma una volta che abbiamo costruito l'itable, questo ci permette di spedire a buon mercato le chiamate di metodo a molte interfacce, qualcosa a cui le tradizionali lingue OOP soffrono. Qui m è il numero di metodi nell'interfaccia di destinazione e b è il numero di classi di base:
- C ++ affetta gli oggetti o ha bisogno di inseguire i puntatori di ereditarietà virtuale durante la trasmissione, ma ha un semplice accesso vtable. O (1) o O (b) costo del upcasting, ma O (1) invio del metodo.
- La Java Hotspot VM non deve fare nulla durante l'upcasting, ma dopo la ricerca del metodo di interfaccia esegue una ricerca lineare attraverso tutti gli oggetti utilizzabili da quella classe. O (1) upcasting, ma invio di metodo O (b).
- Python non deve fare nulla durante l'upcasting, ma usa una ricerca lineare attraverso un elenco di classi base linearizzate C3. O (1) upcasting, ma invio del metodo O (b²)? Non sono sicuro di quale sia la complessità algoritmica di C3.
- .NET CLR utilizza un approccio simile a Hotspot ma aggiunge un altro livello di riferimento indiretto nel tentativo di ottimizzare l'utilizzo della memoria. O (1) upcasting, ma invio di metodo O (b).
La complessità tipica per la distribuzione dei metodi è molto migliore poiché spesso la ricerca dei metodi può essere memorizzata nella cache, ma le complessità del caso peggiore sono piuttosto orribili.
In confronto, Go ha O (1) o O (m) upcasting e l'invio del metodo O (1). Haskell non ha upcasting (vincolare un tipo con una classe di tipo è un effetto in fase di compilazione) e un invio di metodo O (1).