Pular para o conteúdo

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.

dataforge
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#

dataforge
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.

dataforge
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:

dataforge
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 repassou

Tipo 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.

dataforge
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" 
dataforge
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.message

Trait genérico#

dataforge
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 -5

Exigir 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.

dataforge
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.

PerguntaResposta no DataForge
despacho dinâmicosim, sempre: o método vem do objeto
despacho estáticonão existe como escolha — o analisador resolve o nome antes de rodar, mas a chamada é dinâmica
vtablenão há tabela declarável: a busca usa a linhagem e um cache de método por blueprint
trait objectum parâmetro anotado com o trait já é isso: qualquer objeto que o implemente serve
rootchama a implementação de quem declarou o método em execução, resolvendo o diamante por C3
dataforge
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