Pular para o conteúdo

Generics

Uma estrutura que serve para qualquer tipo.

Um genérico é uma estrutura que serve para qualquer tipo. Pilha<T> é uma pilha de qualquer coisa — e o <T> documenta que o que entra é o que sai.

A forma#

dataforge
blueprint Pilha<T>:
    itens: Cluster := []

    action por(x: T):
        self.itens.append(x)

    action tirar() -> T:
        yield self.itens.pop()

// a mesma estrutura, tipos diferentes
numeros := spawn Pilha()
numeros.por(1)
numeros.por(2)
out numeros.tirar()          // 2

textos := spawn Pilha()
textos.por("a")
out textos.tirar()           // a

Em ações#

dataforge
action primeiro<T>(lista: Cluster) -> T:
    yield lista[0]

out primeiro([1, 2, 3])      // 1
out primeiro(["a", "b"])     // a

O <T> diz que o retorno é do mesmo tipo dos itens da lista. É uma afirmação sobre a relação entre entrada e saída, que o nome do tipo sozinho não conseguiria fazer.

Mais de um parâmetro#

dataforge
blueprint Par<K, V>(chave, valor):
    action get_chave() -> K:
        yield self.chave

    action get_valor() -> V:
        yield self.valor

par := spawn Par("idade", 42)
out par.get_chave(), "=", par.get_valor()   // idade = 42

A convenção é T para um tipo qualquer, K/V para chave e valor, E para elemento. Não é regra — qualquer nome que comece com maiúscula serve.

O que o `<T>` faz, e o que não faz#

Acontece?
o analisador estático aceita T como tipo dentro da declaraçãosim
o <T> do blueprint vale nos métodos delesim
T fora da declaração que o criouerro — ele não vaza
verificar em tempo de execução que os dois T batemnão
<T extends Comparable>ainda não — veja o roadmap

A linguagem é de tipagem dinâmica: o parâmetro de tipo documenta e é aceito pelo analisador, mas não é verificado quando o programa roda. É o mesmo que o TypeScript faz ao compilar — os tipos somem, e o que fica é a documentação mais o que o analisador conseguiu provar.

O que continua sendo verificado#

dataforge
action f(x: Integer):
    yield x

f("texto")
// erro: parameter 'x' of action 'f' declared as Integer but got String

Um tipo concreto continua sendo cobrado, em tempo de execução e na análise estática. O genérico não é uma porta para qualquer nome passar: action f(x: NaoExiste) segue sendo erro.

`<` de genérico e `<` de comparação#

dataforge
blueprint Caixa<T>:      // genérico
    action f():
        yield 1

a := 3
b := 5
out a < b                // comparação — segue funcionando

O parser distingue pelo que vem depois: < seguido de um nome de tipo (maiúscula) e um > fechando abre um genérico; qualquer outra coisa é comparação. Total < Limite, com os dois em maiúscula, ainda é comparação — porque falta o > fechando.

Um caso completo#

dataforge
blueprint Resultado<T, E>(valor, erro):
    action ok() -> Boolean:
        yield self.erro is void

    action pegar() -> T:
        given not self.ok():
            trigger "resultado com erro: " + str(self.erro)
        yield self.valor

    action erro_ou<F>(padrao: F) -> F:
        yield self.erro ?? padrao

action dividir(a, b) -> Resultado:
    given b is 0:
        yield spawn Resultado(void, "divisão por zero")
    yield spawn Resultado(a / b, void)

r := dividir(10, 2)
out r.ok(), r.pegar()                    // yes 5.0

ruim := dividir(1, 0)
out ruim.ok(), ruim.erro_ou("sem erro")  // no divisão por zero

Um Resultado<T, E> é o padrão que Rust chama de Result e Haskell de Either: o valor ou o erro, sem exceção no meio. O genérico é o que permite escrevê-lo uma vez e usá-lo em qualquer lugar.

Comparado ao que você conhece#

DataForgeTypeScriptJava
classeblueprint Pilha<T>class Pilha<T>class Pilha<T>
funçãoaction primeiro<T>()function primeiro<T>()<T> T primeiro()
dois parâmetros<K, V><K, V><K, V>
verificado em execuçãonãonãonão (apagamento)
restriçãoainda não<T extends X><T extends X>