Records
Dados imutáveis com igualdade estrutural, valores padrão, métodos e o operador with.
Declarar#
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.
| Linguagem | Equivalente |
|---|---|
| Python | @dataclass(frozen=True) |
| TypeScript | interface / type |
| Java | record |
| Go | struct |
| Rust | struct |
Construir#
u := Usuario("Ana", 30) # posicional
b := Usuario(nome := "Bruno", idade := 25, email := "b@x.com") # nomeadaA 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:
Record 'Usuario' is missing field 'idade'
field 'nome' of record 'Usuario' declared as String but got IntegerIgualdade estrutural#
Esta é a diferença central em relação a um blueprint:
out Usuario("Ana", 30) is Usuario("Ana", 30) # yes
out Usuario("Ana", 30) is Usuario("Ana", 31) # noDuas 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#
u := Usuario("Ana", 30)
u.idade := 31Record '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#
original := Conta("Ana", 1000)
depositado := original with {"saldo": original.saldo + 500}
out original.saldo, depositado.saldo # 1000 1500Leia como "o mesmo que `original`, mas com `saldo` valendo outra coisa".
| Linguagem | Equivalente |
|---|---|
| Python | dataclasses.replace(obj, saldo=…) |
| JavaScript | {...obj, saldo: …} |
| Rust | Conta { 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#
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 1400Nenhuma 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.
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)3x4 12 6x8Quando 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:
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#
{nome, idade} := u
out nome, idade