Pular para o conteúdo

Racional · ensaio 07

Tipos

rationale.md · 90 linhas · 4 min de leitura

A redefinição: inteiros de largura arbitrária, decl unifica struct/enum/interface, satisfação estrutural, e dois despachos (vtable vs monomorfização) escolhidos por necessidade.

Um sistema de tipos faz três trabalhos: descrever dados, descrever contratos, e decidir como chamadas são despachadas. As linguagens mainstream tomam um punhado de decisões aqui que Makoto revisita: larguras fixas (i32/i64), keywords separadas (struct/enum/interface/trait), satisfação declarada (impl Trait for T), e uma estratégia de despacho fixa.

  • Inteiros de largura arbitrária: iN/uN para qualquer N de 1 a 65535 (u7, i128, u777). byte é u8, bit é u1, unificados, a mesma coisa conceitual. A largura mora no tipo e documenta a intenção (“isto cabe em 7 bits”), em vez de ser uma decisão acidental herdada do hardware.
  • decl unifica struct, enum e interface: uma keyword, e o corpo desambigua (campos struct; variantes enum; assinaturas interface). São o mesmo ato (declarar um tipo) no mesmo reino.
  • Satisfação estrutural: se um tipo tem os métodos que a interface exige, ele a satisfaz. Não há implements/impl. O tipo não precisa saber de antemão quais interfaces vai satisfazer.
  • Dois despachos, escolhidos por necessidade: valor-de-interface vtable (um par dado+vtable, montada pelo compilador por tipo, erased); constraint genérica (T + Trait, HKT) monomorfização (especialização estática). E a régua estrita de constraint: + = implementa interface, : = pertence a uma whitelist. Dois símbolos porque são duas perguntas diferentes.

Por que decl unifica três mas não a union? Pela regra de “operação + reino, não forma” (cap. 01). Struct/enum/interface têm corpos de formato diferente e mesmo assim fundem, porque são o mesmo verbo (declarar tipo) no mesmo reino (o sistema de tipos). union tem corpo idêntico ao da struct e mesmo assim fica de fora, porque é outra operação (reinterpretar bytes) em outro reino (o unsafe). Unificar union pela semelhança de superfície seria apagar uma fronteira.

Por que satisfação estrutural? Porque desacopla. A implementação não precisa conhecer a interface para satisfazê-la: você cria um tipo, e mais tarde alguém descobre que ele serve onde a interface era esperada. Some o ritual de impl (e o erro barulhento de esquecê-lo), e a validação acontece no ponto de uso.

Por que dois despachos em vez de um? Porque cada um resolve o que o outro não consegue. A vtable serve quando a ação não devolve o tipo concreto (w.write(...)): um ponteiro gordo uniforme, custo no ponto de uso, zero overhead por objeto. Mas quando o resultado depende do tipo, como em map[A,B](xs: C[A]) -> C[B], que abstrai sobre o construtor C, a vtable não basta; é preciso monomorfizar. A régua: ação que não devolve o tipo do receiver vtable; transformação que devolve monomorfização.

Por que dois símbolos de constraint? Porque um nome não distingue “pertence ao conjunto Foo” de “implementa a interface Foo”. : introduz só whitelist ({List, Set}, pertencimento); + introduz só interface (+ Writeable, implementação). Dois símbolos porque são dois eixos, e fundi-los reabriria a ambiguidade que a explicitude quer matar.

  • Larguras fixas (i32/i64). Forçam a decisão de capacidade para dentro do código do usuário e não documentam intenção; i7 diz o que int esconde.
  • union dentro de decl. Operação e reino diferentes (cap. 01); fundir apagaria a fronteira unsafe.
  • impl Trait for T explícito (Rust). Acoplamento e ritual; esquecer o impl é um erro comum e barulhento. Estrutural remove o passo.
  • Function pointers inline na struct (Zig puro). Inflariam cada instância; a vtable por-tipo compartilha, mantendo o objeto magro.
  • HKT de ordem superior arbitrária (Scala/Haskell). Aridade ≤ 2 (C[_], C[_,_]) cobre a maioria do FP útil; permitir transformadores de monad abriria o abismo de inferência de kind. Poderoso sem ser footgun.

O i32 que silenciosamente transborda porque ninguém decidiu se cabia. O impl esquecido em Rust e o erro de trait-resolution que aparece três camadas de instanciação abaixo, “candidate #3 has wrong return type”. O objeto Zig que engorda porque carrega os próprios ponteiros de método. Cada uma é uma decisão de tipos que vazou custo para onde não devia.

O tipo diz exatamente o que é; a largura mora no tipo. Um decl declara dados ou contrato (o corpo decide); satisfazer é ter os métodos; o despacho é vtable quando a ação não devolve o tipo, monomorfização quando devolve.

A unificação carrega o decl de responsabilidade (o corpo precisa desambiguar três coisas), e a satisfação estrutural troca a declaração explícita por validação no call-site: você não anuncia que satisfaz, mas também não tem surpresa silenciosa, porque o compilador checa onde a interface é exigida. E o HKT raso (aridade ≤ 2) deixa de fora os transformadores de ordem superior, expressividade sacrificada conscientemente para não importar o inferno de inferência.

Próximo: 08 · Comptime e reflexão