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: 7Sufijos 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: 4Capturas 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: -1Literales 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¶
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.