Pular para o conteúdo

Especificação §9

Interfaces

language-design.md §9 · 40 linhas · 3 min de leitura

Não existe Any: “qualquer tipo” é um bound de capacidade

Seção intitulada “Não existe Any: “qualquer tipo” é um bound de capacidade”

Makoto não tem tipo Any/dynamic/interface{} — nenhum valor que habite todo tipo. É a mesma decisão do Top deliberadamente ausente (seção 2): uma coisa que você recebe mas não pode fazer nada com segurança é o lazy-typing que o Optional e o modelo de erros já fecharam. O substituto não é um tipo, é um bound: “qualquer tipo que possa ser usado deste jeito”. Você nunca diz “recebe qualquer coisa”; você diz “recebe qualquer coisa que satisfaça X”, e X é exatamente o comportamento que o código vai invocar. Um tipo é admitido pelo que faz, nunca por mera identidade — a linguagem inteira trabalha tipos através de comportamento.

Por isso uma chamada no formato printf não precisa de Any. Heterogêneo e tipado é um variádico limitado por uma interface: fn log(xs: ...Display) recebe um número qualquer de valores de tipos diferentes, cada um carregando a única capacidade que o corpo usa (to_string), apagado (type-erased) para um valor de interface (abaixo). A interpolação de string é a forma cotidiana da mesma coisa: "{{ a }} {{ b }}" formata cada buraco via Display, checado estaticamente, erro de compilação para um tipo que não a implementa (seção 16) — nunca um <Object@0x7f…> vazado. O bound é o requisito da operação virado o tipo do parâmetro, então “recebe muitos de qualquer tipo” e “sabe o que pode fazer com cada um” são a mesma declaração, não duas.

Basta implementar os métodos; não há declaração explícita de “implements”:

decl Writeable {
fn write(data: []byte) -> error
fn read(buf: mut []byte) -> Result[usize, error]
}

Um método é uma função externa com receiver (estilo Go), a única forma. A struct guarda dados; os métodos são funções externas:

fn (o: MyStruct) write(...) { ... }

Não há campo-de-função-na-struct (estilo Zig): a linguagem só aceita a forma Go. Implementar os métodos já satisfaz Writeable, sem declarar “implements”.

Dispatch: vtable para valor de interface, monomorfização para constraint genérica

Seção intitulada “Dispatch: vtable para valor de interface, monomorfização para constraint genérica”

São dois mecanismos, e antes o doc os confundia sob “sempre vtable”. O que decide qual vale é se você tem um valor de interface ou uma constraint num genérico:

  • Valor de interface (x: Display usado como valor: guardar um Display numa variável, numa lista heterogênea) vtable, estilo Go. O valor carrega um par (ponteiro pro dado, ponteiro pra vtable do tipo concreto); a vtable é montada pelo compilador por tipo, não por instância. O tipo concreto fica apagado (type-erased): você chama os métodos, mas não recupera “isto é uma List”. Custo: o par (dado, vtable) no ponto de uso, sem overhead por objeto, e a struct nunca carrega ponteiros de função inline.
  • Constraint em genérico (fn f[T + Display](x: T), map[C[_] + Mappable]) monomorfização, despacho estático. Cada tipo concreto gera uma cópia especializada (seção 10/13) onde o tipo é conhecido, e a chamada de método é direta, sem vtable e sem apagamento. É necessário quando o retorno menciona o tipo concreto: map devolve C[B], e só dá pra construir um C[B] concreto sabendo C estaticamente; um Mappable apagado por vtable não saberia qual container construir. É o mesmo que a seção 10 já diz pro Serializable ([T + Serializable] resolve em compilação, código reto).

Em resumo: vtable quando você quer esquecer o tipo concreto (heterogeneidade); monomorfização quando você precisa lembrar dele (retorno HKT como C[B], código reto).

As duas formas existem pra toda interface, Serializable incluído. Os exemplos acima todos usam a forma-constraint ([T + Serializable]), porque a serialização em geral conhece o tipo concreto, mas isso é hábito, não restrição: fn f(s: Serializable) recebe um valor de interface dinâmico (o fat pointer, então um []Serializable heterogêneo é expressável), enquanto fn g[T + Serializable](x: T) é a constraint monomorfizada, e as duas até compõem numa mesma assinatura (fn x[T + Serializable](y: Serializable) — o primeiro Serializable limita o genérico T, o segundo tipa o argumento y). A forma-valor funciona porque o serialize do tipo boxeado é gerado (por auto-derivação, seção 10) no ponto do box, onde o tipo concreto ainda é conhecido, e o vtable só aponta pra ele — a capability vazia não despacha método nenhum; ela marca “isto pode ser serializado”.