Os dois conceitos são muito, muito semelhantes. Em linguagens OOP normais, anexamos uma vtable (ou para interfaces: itable) a cada objeto:
| this
v
+---+---+---+
| V | a | b | the object with fields a, b
+---+---+---+
|
v
+---+---+---+
| o | p | q | the vtable with method slots o(), p(), q()
+---+---+---+
Isso nos permite invocar métodos semelhantes a this->vtable.p(this)
.
Em Haskell, a tabela de métodos é mais como um argumento oculto implícito:
method :: Class a => a -> a -> Int
se pareceria com a função C ++
template<typename A>
int method(Class<A>*, A*, A*)
Onde Class<A>
é uma instância de typeclass Class
para type A
. Um método seria chamado como
typeclass_instance->p(value_ptr);
A instância é separada dos valores. Os valores ainda mantêm seu tipo real. Embora as classes tipográficas permitam algum polimorfismo, isso não é subtipo de polimorfismo. Isso torna impossível fazer uma lista de valores que satisfaçam a Class
. Por exemplo, supondo que tenhamos instance Class Int ...
e instance Class String ...
não possamos criar um tipo de lista heterogêneo como [Class]
esse e que possua valores como [42, "foo"]
. (Isso é possível quando você usa a extensão "tipos existenciais", que efetivamente muda para a abordagem Ir).
No Go, um valor não implementa um conjunto fixo de interfaces. Conseqüentemente, ele não pode ter um ponteiro vtable. Em vez disso, ponteiros para tipos de interface são implementados como ponteiros gordos que incluem um ponteiro para os dados, outro ponteiro para a itable:
`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)
O itable é combinado com os dados em um ponteiro gordo quando você converte de um valor comum para um tipo de interface. Depois de ter um tipo de interface, o tipo real dos dados se torna irrelevante. Na verdade, você não pode acessar os campos diretamente sem passar por métodos ou reduzir a interface (o que pode falhar).
A abordagem da Go ao envio de interfaces tem um custo: cada ponteiro polimórfico é duas vezes maior que um ponteiro normal. Além disso, transmitir de uma interface para outra envolve copiar os ponteiros do método para uma nova vtable. Mas uma vez que construímos o itable, isso nos permite despachar chamadas de método baratas para muitas interfaces, algo com o qual as linguagens tradicionais de POO sofrem. Aqui, m é o número de métodos na interface de destino eb é o número de classes base:
- O C ++ faz o fatiamento de objetos ou precisa perseguir ponteiros de herança virtual ao converter, mas depois tem acesso simples à vtable. O (1) ou O (b) custo de upcasting, mas O (1) envio de método.
- A VM Java Hotspot não precisa fazer nada ao fazer upcasting, mas, na consulta do método de interface, faz uma pesquisa linear por todos os itens implementados por essa classe. O (1) upcasting, mas O (b) envio de método.
- O Python não precisa fazer nada ao fazer upcasting, mas usa uma pesquisa linear por meio de uma lista de classes base linearizadas em C3. O (1) upcasting, mas O (b²) despacho de método? Não tenho certeza qual é a complexidade algorítmica do C3.
- O .NET CLR usa uma abordagem semelhante ao Hotspot, mas adiciona outro nível de indireção na tentativa de otimizar o uso da memória. O (1) upcasting, mas O (b) envio de método.
A complexidade típica do envio de métodos é muito melhor, pois a pesquisa de métodos geralmente pode ser armazenada em cache, mas as complexidades de pior caso são bastante horríveis.
Em comparação, Go possui upcasting O (1) ou O (m) e envio de método O (1). Haskell não tem upcasting (restringir um tipo com uma classe type é um efeito em tempo de compilação) e O (1) despacham o método.
[42, "foo"]
. É um exemplo vívido.