Pular para o conteúdo

Records

Dados imutáveis com igualdade estrutural, valores padrão, métodos e o operador with.

Declarar#

dataforge
record Usuario:
    nome: String
    idade: Integer
    email: String := "sem@email"

Cada campo exige um tipo. Um := depois do tipo dá um valor padrão, tornando o campo opcional na construção.

LinguagemEquivalente
Python@dataclass(frozen=True)
TypeScriptinterface / type
Javarecord
Gostruct
Ruststruct

Construir#

dataforge
u := Usuario("Ana", 30)                                # posicional
b := Usuario(nome := "Bruno", idade := 25, email := "b@x.com")  # nomeada

A forma nomeada é preferível com mais de três campos: Usuario("Ana", 30, "a@x.com", yes, 2) é ilegível.

Campo obrigatório ausente ou tipo errado dispara erro na construção — e o check acha antes:

text
Record 'Usuario' is missing field 'idade'
field 'nome' of record 'Usuario' declared as String but got Integer

Igualdade estrutural#

Esta é a diferença central em relação a um blueprint:

dataforge
out Usuario("Ana", 30) is Usuario("Ana", 30)     # yes
out Usuario("Ana", 30) is Usuario("Ana", 31)     # no

Duas instâncias com os mesmos valores são iguais, mesmo sendo objetos distintos. Blueprints comparam por identidade; records, por conteúdo. É isso que torna records úteis como chaves e em comparações de teste.

Imutabilidade#

dataforge
u := Usuario("Ana", 30)
u.idade := 31
text
Record 'Usuario' is immutable: cannot assign to 'idade'.
Build a changed copy with "registro with {'idade': valor}".

Não é uma restrição arbitrária: é o que permite a igualdade estrutural funcionar de forma confiável, e elimina a classe de bugs em que o valor guardado muda debaixo dos seus pés.

with — a cópia alterada#

dataforge
original := Conta("Ana", 1000)
depositado := original with {"saldo": original.saldo + 500}

out original.saldo, depositado.saldo     # 1000 1500

Leia como "o mesmo que `original`, mas com `saldo` valendo outra coisa".

LinguagemEquivalente
Pythondataclasses.replace(obj, saldo=…)
JavaScript{...obj, saldo: …}
RustConta { saldo: …, ..original }
Elixir%{original | saldo: …}

O with também valida os nomes: pedir um campo que não existe é erro. Compare com o spread de JavaScript, onde {...obj, sldo: 1} cria alegremente um campo com o nome digitado errado.

Transformações encadeadas#

dataforge
action depositar(conta, valor):
    yield conta with {"saldo": conta.saldo + valor}

action sacar(conta, valor):
    guard valor smaller_eq conta.saldo, "saldo insuficiente"
    yield conta with {"saldo": conta.saldo - valor}

final := sacar(depositar(original, 500), 100)
out original.saldo, final.saldo     # 1000 1400

Nenhuma das chamadas tocou em original. Se algo der errado no meio, você ainda tem o estado anterior intacto — que é exatamente o que se quer numa transação.

Métodos#

Um record pode ter métodos. A regra não tem exceção: podem ler `self`, nunca escrever.

dataforge
record Retangulo:
    largura: Number
    altura: Number

    action area():
        yield self.largura * self.altura

    action escalar(fator):
        yield Retangulo(self.largura * fator, self.altura * fator)

    action toString():
        yield $"{self.largura}x{self.altura}"

r := Retangulo(3, 4)
out r, r.area(), r.escalar(2)
saída
3x4 12 6x8

Quando um método precisaria mudar o estado, ele devolve um record novo — o mesmo padrão de "abc".upper().

Em pattern matching#

Records se desmontam em padrões, e é aí que o desenho todo se paga:

dataforge
match fig:
    point Retangulo(l, a) when l is a:
        yield "quadrado"
    point Retangulo(largura := 0):
        yield "degenerado"
    point Retangulo:
        yield "retangulo"

point Retangulo(l, a) faz três coisas de uma vez: verifica o tipo, extrai os campos e liga cada um a um nome. Veja Pattern matching.

Desestruturar#

dataforge
{nome, idade} := u
out nome, idade