Saltar al contenido

Guía 22 de 53

Validación, esquemas y unidades

Valores validados, capturas tipadas, schemas y unidades dimensionales.

En esta página

Valores validados, capturas tipadas, schemas y unidades dimensionales.

Newtypes escalares validados

Un newtype con where incorpora el predicado a la construcción. Su constructor new devuelve Result; trata el fallo o propágalo. Las operaciones derivadas mantienen la validación. Un newtype sin where conserva su constructor directo.

Practica en By Example · Programa fuente

// 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

Sufijos de unidades declarados

Asocia @unit a un newtype escalar para dar un sufijo tipado a los literales. Conserva el tipo nominal y la precisión escalar. Un sufijo solo no define conversión: declara dimensiones y escalas para convertir automáticamente.

Practica en By Example · Programa fuente

// 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 en strings

Un literal pat compara la cadena completa y convierte las capturas antes de exponer enlaces. Una conversión escalar o construcción validada fallida significa que no coincide. Usa el primer delimitador sin retroceso; se rechazan capturas adyacentes sin delimitador.

Practica en By Example · Programa fuente

// 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

Literales de cadena nominales validados

Un literal como Email"ana@empresa.com" se acepta solo si el predicado del newtype de cadena validado acepta su contenido estático durante la compilación. Las cadenas dinámicas usan el constructor falible. Se conserva la semántica de los templates etiquetados por funciones.

Practica en By Example · Programa fuente

// expect: ana@empresa.com
// expect: true
// expect: empty
// expect: alias@empresa.com
// 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"ana@empresa.com"
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"alias@empresa.com".unwrap())
fn callable() -> str { let Email = |parts: [str]| parts[0]; Email"callable" }
print(callable())

Patrones de captura como valores

Guarda un literal pat como valor reutilizable. Su método parse devuelve Result con un record de capturas de tipos exactos. Entrada incompatible y conversiones fallidas devuelven errores estructurados. La construcción retiene las funciones de conversión sin ejecutar sus predicados.

Practica en By Example · Programa fuente

// 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

Añade predicados where después de un schema para validar el valor completo. La conversión de campos y los valores por defecto ocurren primero. Los invariantes devuelven bool y se ejecutan en orden; los fallos producen DecodeError estructurado en la ruta del objeto.

Practica en By Example · Programa fuente

// 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)
}

Dimensiones y escalas de unidades

Declara unidades base, dimensiones derivadas y escalas exactas con @unit. Multiplicación y división derivan dimensiones; as convierte solo dimensiones compatibles y comprueba representabilidad. Los sufijos de duración mantienen el comportamiento en milisegundos fuera de un contexto dimensional.

Practica en By Example · Programa fuente

// 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()

Sigue aprendiendo

Documentación · Practica en By Example

DOCS / OPINIÓN

¿Esta página dejó alguna pregunta?

Cuéntanos dónde te perdió la explicación. La documentación también forma parte de la experiencia del lenguaje.

Índice global

Encuentra tu camino por Zolo

Prueba una idea

Empieza aquí

9 resultados

9 resultados

enespt-br