Pular para o conteúdo

Guia 22 de 53

Validação, schemas e unidades

Valores validados, capturas tipadas, schemas e unidades dimensionais.

Nesta página

Valores validados, capturas tipadas, schemas e unidades dimensionais.

Newtypes escalares validados

Um newtype com where incorpora o predicado à construção. Seu construtor new retorna Result; trate a falha ou propague-a. Operações derivadas mantêm a validação. Um newtype sem where preserva o construtor direto.

Pratique na By Example · Programa fonte

// Validation is part of construction: .new returns Result<Score, str>.
newtype Score(int) deriving Eq, Ord, Clone, Default, Add where |v| 0 <= v <= 100
newtype Positive(int) where |v| v > 0
newtype UserId(int)

let score = Score.new(40).unwrap()
let delta = Score.new(2).unwrap()
print(score.unwrap()) // expected: 40
print((score + delta).unwrap()) // expected: 42
print(score.clone() == score) // expected: true
print(Score.default().unwrap()) // expected: 0
print(Score.new(101).is_err()) // expected: true
print(Positive.new(0).is_err()) // expected: true
// Ordinary newtypes still construct directly.
print(UserId.new(7).unwrap()) // expected: 7

Sufixos de unidades declarados

Associe @unit a um newtype escalar para dar um sufixo tipado aos literais. O sufixo preserva o tipo nominal e a precisão do escalar. Um sufixo sozinho não define conversão: declare dimensões e escalas para converter automaticamente.

Pratique na By Example · Programa fonte

// Declared suffixes retain exact scalar spelling and nominal ownership.
@unit("kg") newtype Mass(decimal) where |value| value >= 0d
@unit("g") newtype Grams(decimal)
@unit("crate") newtype Crates(int)

let parcel = 2.5kg
let total = parcel + 0.5kg
print(total.unwrap()) // expected: 3.0
print((parcel * 2d).unwrap()) // expected: 5.0
print(parcel / 0.5kg) // expected: 5
print(parcel > 1kg) // expected: true

let grams = 500g
let converted = Mass.new(grams.unwrap() / 1000d).unwrap()
print(converted.unwrap()) // expected: 0.50

let stock = 8crate
print((stock / 2).unwrap()) // expected: 4
print(stock / 2crate) // expected: 4

Capturas tipadas em strings

Um literal pat compara a string inteira e converte as capturas antes de expor os bindings. Conversão escalar ou construção validada que falha significa não corresponder. O primeiro delimitador é usado sem backtracking; capturas adjacentes sem delimitador são rejeitadas.

Pratique na By Example · Programa fonte

// Complete-string matching; conversions and guards can fall through.
fn order(input: str) -> str {
    match input {
        pat"order/{id: int}/{status: str}" if id > 0 => "{id}:{status}",
        _ => "invalid",
    }
}
print(order("order/42/paid")) // expected: 42:paid
print(order("order/oops/paid")) // expected: invalid
print(order("prefixorder/7/paid")) // expected: invalid
print(order("order/-1/paid")) // expected: invalid

let enabled = match "flag/true" {
    pat"flag/{value: bool}" => value,
    _ => false,
}
print(enabled) // expected: true

let braces = match r"{done}/7" {
    pat"{{done}}/{value: int}" => value,
    _ => -1,
}
print(braces) // expected: 7

newtype Positive(int) where |value| value > 0
let checked = match "id/23" {
    pat"id/{value: Positive}" => value.unwrap(),
    _ => -1,
}
print(checked) // expected: 23
let rejected = match "id/-2" {
    pat"id/{value: Positive}" => value.unwrap(),
    _ => -1,
}
print(rejected) // expected: -1

Literais de string nominais validados

Um literal como Email"[email protected]" só é aceito se o predicado do newtype de string validado aceitar seu conteúdo estático na compilação. Strings dinâmicas continuam usando o construtor falível. Templates prefixados por funções existentes preservam sua semântica.

Pratique na By Example · Programa fonte

// expect: [email protected]
// expect: true
// expect: empty
// expect: [email protected]
// expect: callable
const required = "@"
fn valid_email(value: str) -> bool = value.contains(required)
newtype Email(str) where |value| valid_email(value)
newtype Empty(str) where |value| value == ""
let email = Email"[email protected]"
print(email.unwrap())
let input = "invalid"
print(Email.new(input).is_err())
let empty = Empty""
print(if empty.unwrap() == "" { "empty" } else { "wrong" })
mod address { pub newtype Address(str) where |value| value.contains("@") }
use address::{Address as Mail}
print(Mail"[email protected]".unwrap())
fn callable() -> str { let Email = |parts: [str]| parts[0]; Email"callable" }
print(callable())

Padrões de captura como valores

Guarde um literal pat como valor reutilizável. Seu método parse retorna Result com um record de capturas de tipos exatos. Entrada incompatível e conversões que falham retornam erros estruturados. Construir o padrão retém as funções de conversão sem executar seus predicados de validação.

Pratique na By Example · Programa fonte

// Exact public capture record; construction performs no parse or validation.
var input_calls = 0
fn input() -> str { input_calls += 1; "users/9223372036854775807/false" }
let route = pat"users/{id: int}/{ready: bool}"
let captured = route.parse(input()).unwrap()
print(captured.id)
print(captured.ready)
print(input_calls)
print(route.parse("prefix/users/1/true").is_err())
print(route.parse("users/9223372036854775808/true").is_err())
print(route.parse("users/1/true/trailing").is_err())
match route.parse("users/nope/true") {
    .Err(error) => { print(error.kind); print(error.input) },
    .Ok(_) => print("unexpected"),
}
let id = "outer"
let empty = pat""
print(empty.parse("").is_ok())
print(empty.parse("x").is_err())
let escaped = pat"{{literal}}/{id: str}"
print(escaped.parse(r"{literal}/é中😀").unwrap().id)
print(id)
newtype Positive(int) where |n| n > 0
let positive = pat"n/{value: Positive}"
print(positive.parse("n/3").unwrap().value.unwrap())
print(positive.parse("n/-1").is_err())
var cleanup = ""
struct ScopeResource {}
impl core::cleanup::Cleanup for ScopeResource { fn close(self) { cleanup += "closed" } }
using resource = ScopeResource {} {
    let parsed = route.parse("bad")
    defer { cleanup += "defer/" }
    print(parsed.is_err())
}
print(cleanup)
fn parse_with_cleanup() -> Result<int, { kind: str, input: str, pattern: str }> {
    using resource = ScopeResource {} {
        defer { cleanup += "defer/" }
        let args = route.parse("bad")?;
        .Ok(args.id)
    }
}
cleanup = ""
print(parse_with_cleanup().is_err())
print(cleanup)
var construction_calls = 0
fn make_route() { construction_calls += 1; pat"users/{id: int}" }
print(make_route().parse("users/8").unwrap().id)
print(construction_calls)

Invariantes entre campos de schemas

Adicione predicados where após um schema para validar o valor completo. Conversão de campos e defaults acontecem primeiro. Invariantes retornam bool e executam na ordem; falhas viram DecodeError estruturado no caminho do objeto. Restrições de campos mantêm seu próprio escopo.

Pratique na By Example · Programa fonte

// Item 13: field checks -> defaults -> assembled-object invariants, once each.
var trace = ""
var cleaned = 0
fn last_default() -> int { trace += "D"; 10 }
fn nonnegative(value: int) -> bool { trace += "F"; value >= 0 }
schema Booking {
    start: int where nonnegative,
    end: int = last_default(),
}
where |booking| {
    defer { cleaned += 1 }
    trace += "I"
    booking.start < booking.end
}
where |booking| { trace += "J"; booking.end <= 20 }

fn show(result: Result<Booking, DecodeError>) {
    match result {
        .Ok(booking) => print("ok:{booking.start}:{booking.end}"),
        .Err(error) => {
            print("{error.path}:{error.kind}:{error.expected}:{error.actual}:{error.message}")
            print(error.causes.len())
        },
    }
}
fn main() {
    let valid = Booking.parse(#{start: 2})
    assert(match valid { .Ok(value) => value.start == 2 && value.end == 10, .Err(_) => false })
    assert(trace == "FDIJ")
    show(valid)
    print(trace)
    trace = ""

    let invariant_failure = Booking.parse(#{start: 10, end: 1})
    assert(match invariant_failure {
        .Err(error) => error.path == "$" && error.expected == "Booking" && error.actual == "Booking"
            && error.message == "Booking: invariant 1 failed" && error.causes.len() == 0,
        .Ok(_) => false,
    })
    assert(trace == "FI")
    show(invariant_failure)
    print(trace)
    trace = ""

    let field_failure = Booking.parse(#{start: -1})
    assert(match field_failure {
        .Err(error) => error.path == "$.start" && error.message == "start: constraint failed",
        .Ok(_) => false,
    })
    assert(trace == "F")
    show(field_failure)
    print(trace)
    trace = ""

    let wrong_type = Booking.parse(#{start: 2, end: "bad"})
    assert(match wrong_type {
        .Err(error) => error.path == "$.end" && error.expected == "int" && error.actual == "string",
        .Ok(_) => false,
    })
    assert(trace == "F")
    show(wrong_type)
    print(trace)
    print(cleaned)
    assert(cleaned == 2)
}

Dimensões e escalas de unidades

Declare unidades base, dimensões derivadas e escalas exatas com @unit. Multiplicação e divisão derivam dimensões; as converte apenas dimensões compatíveis e verifica representabilidade. Sufixos de duração mantêm o comportamento em milissegundos fora de um contexto dimensional.

Pratique na By Example · Programa fonte

// Exact dimensional scales; standalone durations retain milliseconds.
@unit("m", base: true) newtype Meters(decimal) where |v| v >= 0d
@unit("km", of: Meters, scale: 1000d) newtype Kilometers(decimal)
@unit("s", base: true, duration: true) newtype Seconds(decimal)
@unit("h", of: Seconds, scale: 3600d, duration: true) newtype Hours(decimal)
@unit("kph", dimension: Kilometers / Hours) newtype Kph(decimal)
@unit("kmh", dimension: Kilometers * Hours) newtype Kmh(decimal)
fn main() {
    let distance = 2km as Meters
    assert(distance.unwrap() == 2000d)
    print(distance.unwrap())
    let speed = 80km / 2h
    let displayed = speed as Kph
    assert(displayed.unwrap() == 40d)
    print(displayed.unwrap())
    let covered = (speed * 2h) as Kilometers
    assert(covered.unwrap() == 80d)
    print(covered.unwrap())
    let reverse = (2h * 80km) as Kmh
    assert(reverse.unwrap() == 160d)
    print(reverse.unwrap())
    assert(((2km / 500m) as decimal) == 4d)
    print((2km / 500m) as decimal)
    let wait = 2h
    assert(wait == 7_200_000)
    print(wait)
    let hours: Hours = 2h
    assert(hours.unwrap() == 2d)
    print(hours.unwrap())
    let nano: Seconds = 1ns
    assert(nano.unwrap() == 0.000000001d)
    print(nano.unwrap())
    assert(1ns == 0)
    print(1ns)
    let clock = 90_061_000
    print("{clock as h}:{clock as min}:{clock as s}:{clock as ms}")
}

main()

Continue aprendendo

Documentação · Pratique na By Example

DOCS / OPINIÃO

Esta página deixou alguma pergunta?

Conte onde a explicação perdeu você. A documentação também faz parte da experiência da linguagem.

Índice global

Encontre seu caminho pela Zolo

Experimente uma ideia

Comece por aqui

9 resultados

9 resultados

enespt-br