Interfaces
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.
Satisfação implícita (Go-style)
Seção intitulada “Satisfação implícita (Go-style)”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]}Como implementar
Seção intitulada “Como implementar”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: Displayusado como valor: guardar umDisplaynuma 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 é umaList”. 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:mapdevolveC[B], e só dá pra construir umC[B]concreto sabendoCestaticamente; umMappableapagado por vtable não saberia qual container construir. É o mesmo que a seção 10 já diz proSerializable([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”.