Sistema de traits
Traits com implementação padrão, herança entre traits, tipos e constantes associados, interseção, despacho dinâmico e o que a linguagem cobra de quem implementa.
Um trait descreve o que um objeto sabe fazer. Um método sem corpo é exigência; um método com corpo é implementação padrão, que quem adota recebe de graça.
trait Legivel:
action ler() // exigência
action descrever(): // padrão: vem junto
yield $"leio: {self.ler()}"
blueprint Documento extends Legivel:
conteudo := "vazio"
action ler():
yield self.conteudo
d := spawn Documento()
assert d.ler() is "vazio"
assert d.descrever() is "leio: vazio" A prova de que a variância não tem o que decidir#
blueprint Caixa<T>(valor: T):
action ler() -> T:
yield self.valor
inteira: Caixa<Integer> := spawn Caixa(7)
// Covariancia, de graca: um Integer E um Number
larga: Caixa<Number> := inteira
assert larga.ler() is 7
// E o incompativel e recusado — sem nenhuma declaracao de variancia
monitor:
errada: Caixa<String> := inteira
assert no
handle TypeError as e:
assert "declared as String but got Integer" in e.message
out "a conferencia estrutural ja responde assignability" O preço da escolha estrutural, nomeado: ela custa uma passada pelos campos em cada atribuição anotada, e não decide nada antes de rodar para um valor que o analisador não vê. Um sistema nominal decide estaticamente e de graça — e precisa da declaração de variância para isso.
Um trait herda de outro#
trait Editavel extends Legivel soma as exigências e as implementações padrão. A mensagem de quem não implementa aponta onde a exigência nasceu, e não quem a repassou — numa cadeia de traits, o nome errado manda procurar no arquivo errado.
trait Legivel:
action ler()
action descrever():
yield $"leio: {self.ler()}"
trait Editavel extends Legivel:
action escrever(x)
blueprint Nota extends Editavel:
texto := ""
action ler():
yield self.texto
action escrever(x):
self.texto := x
n := spawn Nota()
n.escrever("oi")
assert n.ler() is "oi"
assert n.descrever() is "leio: oi" Quem implementa só metade é recusado na declaração, e a mensagem diz de onde vem a exigência:
trait Legivel:
action ler()
trait Editavel extends Legivel:
action escrever(x)
monitor:
blueprint Meio extends Editavel:
action escrever(x):
yield x
assert no
handle TraitContractError as e:
assert "ler" in e.message
assert "Legivel" in e.message // quem exigiu, não quem repassouTipo associado e constante associada#
Um trait pode declarar um tipo que quem implementa preenche, e uma constante que todos compartilham. O tipo associado é usado nas anotações dos métodos, e é conferido em execução como qualquer anotação.
trait Coletor:
type Item := Any // tipo associado
steady LIMITE := 3 // constante associada
action pegar() -> Item
blueprint Fila extends Coletor:
type Item := Integer // preenchido aqui
itens := [1, 2]
action pegar() -> Item:
yield self.itens[0]
f := spawn Fila()
assert f.pegar() is 1
assert Fila.LIMITE is 3
assert Fila.Item is "Integer" trait Coletor:
type Item := Any
action pegar() -> Item
blueprint Errada extends Coletor:
type Item := Integer
action pegar() -> Item:
yield "nao e numero"
monitor:
(spawn Errada()).pegar()
assert no
handle TypeError as e:
assert "Integer" in e.message and "String" in e.messageTrait genérico#
trait Comparavel<T>:
action comparar(outro: T) -> Integer
blueprint Dinheiro extends Comparavel:
valor := 0
action comparar(outro: Dinheiro) -> Integer:
yield self.valor - outro.valor
a := spawn Dinheiro()
b := spawn Dinheiro()
b.valor := 5
assert a.comparar(b) is -5Exigir dois traits ao mesmo tempo#
A interseção de tipos (&) é o que diz "precisa saber as duas coisas" sem inventar um trait novo só para juntá-las. Ver tipos nomeados.
trait Serial:
action serializar()
trait Ordenavel:
action comparar(outro)
type Auditavel := Serial & Ordenavel
blueprint Lancamento extends Serial, Ordenavel:
valor := 7
action serializar():
yield $"L{self.valor}"
action comparar(outro):
yield self.valor - outro.valor
action registrar(x: Auditavel) -> String:
yield x.serializar()
assert registrar(spawn Lancamento()) is "L7" Despacho: dinâmico por padrão#
A chamada de método resolve pelo objeto, em execução, subindo a linhagem (C3, como o super() do Python). Não há vtable a declarar nem virtual a escrever: todo método é despachado assim.
| Pergunta | Resposta no DataForge |
|---|---|
| despacho dinâmico | sim, sempre: o método vem do objeto |
| despacho estático | não existe como escolha — o analisador resolve o nome antes de rodar, mas a chamada é dinâmica |
| vtable | não há tabela declarável: a busca usa a linhagem e um cache de método por blueprint |
| trait object | um parâmetro anotado com o trait já é isso: qualquer objeto que o implemente serve |
root | chama a implementação de quem declarou o método em execução, resolvendo o diamante por C3 |
trait Forma:
action area()
blueprint Quadrado extends Forma:
lado := 2
action area():
yield self.lado ** 2
blueprint Circulo extends Forma:
raio := 1
action area():
yield 3.14 * self.raio ** 2
action somar_areas(formas: Cluster<Forma>) -> Float:
total := 0.0
cycle f in formas:
total += f.area() // despacho dinâmico
yield total
assert round(somar_areas([spawn Quadrado(), spawn Circulo()]), 2) is 7.14