Sistema de tipos
Princípios
Seção intitulada “Princípios”- Expressivo mas simples: tipos algébricos (sum types, product types), generics, pattern matching, sem borrow checker, sem lifetimes
- Explícito e “matematicamente provável”: o compilador consegue raciocinar sobre o código sem análise de lifetime
- Sem classes: composição sobre herança, sempre
O que existe
Seção intitulada “O que existe”- Structs (product types)
- Enums / sum types
- Interfaces (ver seção 9)
- Generics (ver seção 10)
- Pattern matching exaustivo
unioncru: reinterpretação de bytes, unsafe, para FFI e bit-tricks (escape-hatch; ver seção 5)
Declarando tipos: decl
Seção intitulada “Declarando tipos: decl”Uma keyword, decl, declara struct, enum e interface, e o corpo diz qual dos três, como match comuta sobre o sujeito e loop repete sobre a condição. Não há struct/enum/interface como keywords separadas: o ato é definir uma destas três formas, e o corpo (campos, variantes ou assinaturas) desambigua qual.
decl Point { x: int; y: int } // campos 'nome: tipo' → produto (struct)decl Shape { Circle(int); Square } // variantes 'Nome(...)' → soma (enum)decl Drawable { fn draw() } // assinaturas 'fn ...' → comportamento (interface)Campo (nome: tipo), variante (Nome/Nome(...)) e assinatura (fn ...) são sintaticamente distintos, e como método é função-com-receiver à parte (abaixo), um corpo é homogêneo, nunca mistura os três. “Struct”, “enum” e “interface” seguem sendo os nomes das três formas (produto, soma, comportamento); decl é a única keyword.
E há uma razão mais funda pra uma keyword só dar conta dos três: eles são, todos, contratos. Uma struct é um contrato de estrutura: estes campos, com estes tipos. Uma enum é um contrato de valores: uma destas variantes, e nenhuma fora da lista. Uma interface é um contrato de comportamento: estas funções existem, com estas assinaturas. O que struct/enum/interface sempre compartilharam não foi a forma do corpo; foi o ato, e o ato é um só, declarar um contrato, que é exatamente o que decl nomeia. O corpo só diz sobre o quê o contrato é.
Um enum é uma soma fechada, e suas variantes formam um domínio que se sub-seta: Shape{Circle} é “um Shape, garantido Circle”, um conjunto de variantes, a mesma mecânica que unifica com os error-sets (seção 7).
Um tipo recursivo passa por indireção. Um struct ou enum não pode conter a si mesmo por valor — direto (decl Node { next: Node }) ou transitivamente através de outro agregado por-valor (decl Cons { head: int; tail: Optional[Cons] }) — porque um layout flat teria tamanho infinito. A auto-referência passa por um ponteiro *Node, um many-pointer [*]Node, ou um slice []Node, cada um uma alça de tamanho fixo pra outro lugar, então decl Node { value: int; next: Optional[[*]Node] } é a forma de um nó de lista. É a regra que C e Rust compartilham (o Box do Rust): o compilador reporta o ciclo por-valor e nomeia a correção, ele nunca boxa em silêncio — alocação escondida quebraria o contrato “você só paga pelo que invoca” da seção 5.
O union fica de fora: seu corpo é campos, idêntico ao do produto, então o corpo não o desambigua. Precisa de keyword própria, e ela carrega o unsafe que o sistema de reinterpretação de bytes exige.
O corpo vazio: Unit, Never e o Top deliberadamente ausente. O corpo vazio de cada forma é um tipo distinto, e a habitação os mantém separados. decl Marker {} é a struct vazia, um Unit: habitado por exatamente um valor de tamanho-zero — um marcador/tag (o slot de valor de um Set[T], um parâmetro phantom, um token “isto aconteceu”). decl Never { _ } é o enum vazio, o tipo Never: zero variantes, não-habitado, nenhum valor construível — o bottom na posição de valor (o mesmo {} não-habitado que error{} nomeia do lado dos error-sets, seção 7, e que noreturn nomeia na posição de retorno, seção 14). O _ solitário no corpo é o marcador explícito de “sem variantes”; um {} de fato vazio resolve pra struct (Unit), o mais útil dos três, então o Never pede o _ pra dizer “eu quis dizer zero, não esqueci”. A interface vazia seria o Top: nenhum comportamento exigido, então todo tipo a satisfaz — que é exatamente Any, e Makoto deliberadamente não tem como escrevê-lo (seção 9). A assimetria é o ponto: Unit (um valor) e Never (nenhum valor) são expressáveis; Top (todos os valores, i.e. um bound que não promete nada) não é, porque um contrato que não garante comportamento não garante nada que você possa fazer, e reabre o lazy-typing que Optional e as interfaces fecharam. Unit / Never / Top — dois você nomeia, um você não pode, de propósito.
Apelidos de tipo: alias
Seção intitulada “Apelidos de tipo: alias”decl cria estrutura nova. Quando você só quer um segundo nome pra um tipo que já existe, é alias:
alias UUID = u8 // UUID é u8, intercambiávelÉ um synonym transparente: UUID e u8 são o mesmo tipo, então uuid + 32 compila (UUID é u8). A linguagem já trazia isso embutido (char é apelido de grapheme, seção 16); alias é a keyword que nomeia o ato. Um tipo distinto, com identidade própria e não-intercambiável, não é alias: é um decl (uma struct envolvendo o valor).
O = separa os dois atos: decl Point { ... } não leva =, porque o corpo {...} é a definição (o tipo nasce da estrutura); alias UUID = u8 leva =, porque não há estrutura nova, só a equação “este nome é aquele tipo”. Tem = apelido (já existe); não tem estrutura nova. E type fica intocado: continua sendo só o kind (T: type, -> type, seção 10). alias nomeia tipos, type é o tipo dos tipos.
O que não existe
Seção intitulada “O que não existe”- Classes / herança
- Borrow checker
- Lifetimes explícitos
Box<Arc<'_Type<'c<uint>>>>e similares
Linearidade de escopo (sem borrow checker)
Seção intitulada “Linearidade de escopo (sem borrow checker)”Em vez de lifetimes, a garantia vem da linearidade de escopo nos pontos de transferência: quando um valor é enviado com @transfer, o compilador invalida a variável no escopo atual imediatamente. Não há “empréstimo”: ou o dado é seu, ou você transferiu. O compilador só precisa verificar: “essa variável já foi transferida?”. Se sim, erro de compilação. (Essa mesma ideia de checagem local reaparece na mutação dentro de um processo, com mut e ponteiros; ver seção 6.)
Composição: @embeds
Seção intitulada “Composição: @embeds”A linguagem não tem herança: composição é o mecanismo de reúso, no modelo do Go (embedding), mas com a promoção declarada em vez de inferida de um campo anônimo. Dois fatos do modelo de tipos que isso pressupõe:
- Struct só tem campos. Métodos não vivem dentro do struct; são funções livres com um receiver:
fn (d: *Dog) speak() -> string { ... }(modelo Go; o.da chamada é o UFCS da seção 14). - Métodos são funções normais, então levam generics próprios:
fn (d: *Data) compute[T](x: T) -> T { ... }. O[T](seção 10) funciona num método como em qualquer função, porque um método é uma função com receiver, não um construto à parte. - Funções associadas (construtores, fábricas) têm o receiver sem binding:
fn (Arena) new() -> Arena. O tipo no receiver, sem nome, marca “associada ao tipo, sem instância”; com nome (fn (a: Arena) used()) é método, sem nome é associada. A chamada é por ponto, como qualquer membro do tipo:Arena.new(),string.from[u8](bytes), o mesmo.do UFCS (seção 14), resolvendo um membro associado em vez de um método de instância. (Sem keywordstatic: a presença ou ausência do binding já é a marca, estaticarrastaria a conotação de linkage do C, coloring disfarçado.)
O @embeds(Tipo, methods|fields|all) declara que o struct compõe outro tipo, promovendo o que você especifica (métodos, campos, ou tudo); o promovido vira acessível direto no embarcador:
decl Animal {}fn (a: *Animal) speak() -> string { return "..." }
@embeds(Animal, methods)decl Dog { animal: Animal nome: string}
d.speak() // promovido: chama Animal.speak direto, sem citar 'animal'd.animal.speak() // o caminho nomeado também funcionaO que separa isso do embedding do Go é que a promoção é anunciada: o @embeds no topo diz “este struct promove métodos do Animal”. No Go, a promoção é inferida de um campo anônimo (um Animal sem nome perdido no struct), e nada na definição avisa que métodos vão subir. Aqui você lê o decorator e sabe: a promoção é explícita, não um efeito colateral de omitir um nome.
Duas posições, mesma regra da linguagem. O @embeds segue a convenção que o asm já usa (seção 18): decorator em cima para config do todo (como @supervisor acima do spawn), decorator ao lado para ligar a um campo específico (como @asm(in, …) no parâmetro). Você escolhe pela legibilidade: em cima é enxuto pra struct pequena; ao lado lê melhor quando a struct cresce (cada campo mostra, na sua linha, que promove):
// EM CIMA: config do todo (anônimo, no nível da struct)@embeds(Animal, methods)decl Dog { animal: Animal nome: string}
// AO LADO: liga a promoção ao campodecl Robot { motor: Motor @embeds(methods) // campo 'motor: Motor', promove os métodos sensors: Sensors @embeds(all) // campo 'sensors: Sensors', promove tudo serie: string}A diferença entre as duas formas do @embeds é só onde o tipo mora, e isso decorre de ter ou não um campo:
- Campo nomeado:
campo: Tipo @embeds(what). O campo é normal (motor: Motor), e o@embeds(what)só anota o que promover; o tipo está no campo, não se repete. (whatémethods/fields/all.) - Anônimo:
@embeds(Tipo, what)sozinho. Sem campo antes, então o tipo vai no decorator (a única forma de dizê-lo). É o embedding sem nome.
E as posições coexistem na mesma struct (como @supervisor pode estar em cima ou antes do spawn): você pode ter @embeds em cima, ao lado de campos, e anônimo dentro, tudo de uma vez:
@embeds(LivingThing, all) // (em cima) embarca LivingThing no nível da structdecl Android { brain: Brain @embeds(methods) // (ao lado, nomeado) campo 'brain: Brain', promove métodos power: Power @embeds(all) // (ao lado, nomeado) campo 'power: Power', promove tudo @embeds(Chassis, fields) // (anônimo, dentro) sem campo → o tipo vai no decorator serie: string}Aqui o Android promove de quatro fontes ao mesmo tempo (LivingThing de cima, Brain e Power dos campos, e Chassis anônimo), e a regra de conflito abaixo vale sobre tudo que é promovido, não importa de qual posição veio.
Campo nomeado vs. anônimo, e o conflito. Você pode embarcar com nome (animal: Animal) ou sem (Animal solto). A diferença só aparece no conflito de método:
// ERRO: anônimo + colisão de método@embeds(Animal, methods)decl Dog { Animal // Dog e Animal ambos têm speak() → d.speak() seria ambíguo nome: string}
// OK: nomeie o campo, e a colisão se resolve pelo caminho@embeds(Animal, methods)decl Dog { animal: Animal nome: string}fn (d: *Dog) speak() -> string { return "Woof" }fn (a: *Animal) speak() -> string { return "..." }
d.speak() // Dog.speak, o própriod.animal.speak() // Animal.speak, pelo caminho nomeado- Sem colisão o anônimo funciona limpo (
d.speak()chama o do Animal, sem nome). - Com colisão o anônimo é erro de compilação (
d.speak()ambíguo, sem como desambiguar). A correção é dar nome ao campo.
Sem shadowing, a promoção é seletiva. Com campo nomeado + @embeds, promovem-se os métodos que não colidem (você chama d.qualquer_metodo_do_animal() direto), e só os que colidem caem pro caminho nomeado (d.animal.speak()). O método do embarcado nunca some: quando há colisão, o próprio (d.speak()) ganha, mas o do Animal continua com endereço claro (d.animal.speak()), não escondido. É a diferença de “sem shadowing”: o Go deixaria o Dog.speak mascarar o Animal.speak silenciosamente; aqui os dois têm nome, e a colisão se resolve por caminho explícito, não por uma regra invisível de quem-ganha.
A mesma regra vale pra embeds irmãos (vários @embeds no mesmo struct): se nenhum método colide entre os embarcados, nada precisa de nome; se dois irmãos têm o mesmo método, os campos precisam de nome pra desambiguar.
Satisfação de interface segue da promoção. Se @embeds(Animal, methods) promove métodos que satisfazem uma interface (seção 9), o embarcador a satisfaz, porque os métodos são dele agora. E não é mágico: segue do @embeds visível no topo. Animal satisfaz Speaker, Dog embarca promovendo métodos Dog satisfaz Speaker.
Extrair a parte embarcada: b as A. O embedding promove métodos/campos para acesso, mas B não vira A: um tipo concreto é nominal (do_something(value: A) quer um A, não “algo que embarca A”). Quando você precisa passar a parte-A de um B, a extração é explícita com as:
fn do_something(value: A) { ... }
b := B{ ... } // B embarca A (@embeds(A, all))do_something(b) // ERRO: B não é A (nominal)do_something(b as A) // OK: extrai o A embarcado, dropando o que é específico de Bb as A produz o A embarcado por valor (cópia). É a única forma quando o embed é anônimo (não há b.a para acessar); para embed nomeado é um sinônimo de b.a. A regra mantém a explicitude: nada de B-vira-A implícito, você escreve a conversão. (Para uso estrutural, “qualquer coisa com estes métodos”, o caminho é uma interface, não o tipo concreto; o embedding faz o embarcador satisfazer as interfaces do embarcado, seção 9.)