Recoverable Errors with Result
La mayoría de los errores no son lo suficientemente graves como para que el programa se detenga por completo. A veces, cuando una función falla, es por una razón que usted puede interpretar fácilmente y a la que puede responder. Por ejemplo, si intenta sumar dos enteros grandes y la operación se desborda porque la suma excede el valor máximo representable, es posible que desee devolver un error o un resultado envuelto en lugar de causar un comportamiento indefinido o terminar el proceso.
The Result
enum
Recall from “Generic data types” in Chapter 8 that the Result
enum is defined as having two variants, Ok
and Err
, as follows:
enum Result<T, E> {
Ok: T,
Err: E,
}
El enum Result<T, E>
tiene dos tipos genéricos, T
y E
, y dos variantes: Ok
que tiene el valor de tipo T
y Err
que tiene el valor de tipo E
. Esta definición hace que sea conveniente usar el enum Result
en cualquier lugar donde tengamos una operación que pueda tener éxito (devolviendo un valor de tipo T
) o fallar (devolviendo un valor de tipo E
).
The ResultTrait
El rasgo ResultTrait
proporciona métodos para trabajar con el enum Result<T, E>
, como desenvolver valores, comprobar si el Result
es Ok
o Err
, y entrar en pánico con un mensaje personalizado. La implementación de ResultTraitImpl
define la lógica de estos métodos.
trait ResultTrait<T, E> {
fn expect<+Drop<E>>(self: Result<T, E>, err: felt252) -> T;
fn unwrap<+Drop<E>>(self: Result<T, E>) -> T;
fn expect_err<+Drop<T>>(self: Result<T, E>, err: felt252) -> E;
fn unwrap_err<+Drop<T>>(self: Result<T, E>) -> E;
fn is_ok(self: @Result<T, E>) -> bool;
fn is_err(self: @Result<T, E>) -> bool;
}
Los métodos expect
y unwrap
se parecen en que ambos intentan extraer el valor de tipo T
de un Resultado<T, E>
cuando está en la variante Ok
. Si el Resultado
es Ok(x)
, ambos métodos devuelven el valor x
. Sin embargo, la diferencia clave entre los dos métodos radica en su comportamiento cuando el Result
está en la variante Err
. El método expect
te permite proporcionar un mensaje de error personalizado (como un valor felt252
) que se utilizará cuando se produzca el pánico, dándote más control y contexto sobre el pánico. Por otro lado, el método unwrap
entra en pánico con un mensaje de error por defecto, proporcionando menos información sobre la causa del pánico.
Los métodos expect_err
y unwrap_err
tienen el comportamiento exactamente opuesto. Si el Result
es Err(x)
, ambos métodos devuelven el valor x
. Sin embargo, la diferencia clave entre los dos métodos está en el caso de Result::Ok()
. El método expect_err
te permite proporcionar un mensaje de error personalizado (como un valor felt252
) que se utilizará cuando se produzca el pánico, dándote más control y contexto sobre el pánico. Por otro lado, el método unwrap_err
entra en pánico con un mensaje de error por defecto, proporcionando menos información sobre la causa del pánico.
A careful reader may have noticed the <+Drop<T>>
and <+Drop<E>>
in the first four methods signatures. This syntax represents generic type constraints in the Cairo language. These constraints indicate that the associated functions require an implementation of the Drop
trait for the generic types T
and E
, respectively.
Por último, los métodos is_ok
y is_err
son funciones de utilidad proporcionadas por el rasgo ResultTrait
para comprobar la variante de un valor del enum Result
.
is_ok
toma una instantánea de un valor Result<T, E>
y devuelve true
si el Result
es la variante Ok
, lo que significa que la operación se ha realizado correctamente. Si el Resultado
es la variante Err
, devuelve false
.
is_err
toma una referencia a un valor Result<T, E>
y devuelve true
si el Result
es la variante Err
, lo que significa que la operación ha encontrado un error. Si el Resultado
es la variante Ok
, devuelve false
.
Estos métodos son útiles cuando se desea comprobar el éxito o el fracaso de una operación sin consumir el valor del Resultado, lo que permite realizar operaciones adicionales o tomar decisiones basadas en la variante sin desenvolverla.
Puede encontrar la implementación del ResultTrait
aquí.
Siempre es más fácil entender con ejemplos.
Eche un vistazo a la firma de esta función:
fn u128_overflowing_add(a: u128, b: u128) -> Result<u128, u128>;
Toma dos enteros u128, a y b, y devuelve un Result<u128, u128>
donde la variante Ok
contiene la suma si la suma no se desborda, y la variante Err
contiene el valor desbordado si la suma se desborda.
Ahora, podemos utilizar esta función en otros lugares. Por ejemplo:
fn u128_checked_add(a: u128, b: u128) -> Option<u128> {
match u128_overflowing_add(a, b) {
Result::Ok(r) => Option::Some(r),
Result::Err(r) => Option::None,
}
}
Here, it accepts two u128 integers, a and b, and returns an Option<u128>
. It uses the Result
returned by u128_overflowing_add
to determine the success or failure of the addition operation. The match expression checks the Result
from u128_overflowing_add
. If the result is Ok(r)
, it returns Option::Some(r)
containing the sum. If the result is Err(r)
, it returns Option::None
to indicate that the operation has failed due to overflow. The function does not panic in case of an overflow.
Let's take another example demonstrating the use of unwrap
.
First we import the necessary modules:
use core::traits::Into;
use traits::TryInto;
use option::OptionTrait;
use result::ResultTrait;
use result::ResultTraitImpl;
En este ejemplo, la función parse_u8
toma un entero felt252
e intenta convertirlo en un entero u8
utilizando el método try_into
. Si tiene éxito, devuelve Result::Ok(value)
, en caso contrario devuelve Result::Err('Invalid integer')
.
fn parse_u8(s: felt252) -> Result<u8, felt252> {
match s.try_into() {
Option::Some(value) => Result::Ok(value),
Option::None => Result::Err('Invalid integer'),
}
}
Nuestros dos casos de prueba son:
fn parse_u8(s: felt252) -> Result<u8, felt252> {
match s.try_into() {
Option::Some(value) => Result::Ok(value),
Option::None => Result::Err('Invalid integer'),
}
}
#[cfg(test)]
mod tests {
use super::parse_u8;
#[test]
fn test_felt252_to_u8() {
let number: felt252 = 5_felt252;
// should not panic
let res = parse_u8(number).unwrap();
}
#[test]
#[should_panic]
fn test_felt252_to_u8_panic() {
let number: felt252 = 256_felt252;
// should panic
let res = parse_u8(number).unwrap();
}
}
La primera prueba una conversión válida de felt252
a u8
, esperando que el método unwrap
no entre en pánico. La segunda función de prueba intenta convertir un valor que está fuera del rango u8
, esperando que el método unwrap
entre en pánico con el mensaje de error 'Invalid integer'.
We could have also used the #[should_panic] attribute here.
The ?
operator ?
El último operador del que hablaremos es el operador ?
. El operador ?
se utiliza para un manejo de errores más idiomático y conciso. Cuando usas el operador ?
en un tipo Result
u Option
, hará lo siguiente:
- If the value is
Result::Ok(x)
orOption::Some(x)
, it will return the inner valuex
directly. - If the value is
Result::Err(e)
orOption::None
, it will propagate the error orNone
by immediately returning from the function.
El operador ?
es útil cuando se desea manejar los errores implícitamente y dejar que la función de llamada se ocupe de ellos.
Aquí un ejemplo.
fn do_something_with_parse_u8(input: felt252) -> Result<u8, felt252> {
let input_to_u8: u8 = parse_u8(input)?;
// DO SOMETHING
let res = input_to_u8 - 1;
Result::Ok(res)
}
La función do_something_with_parse_u8
toma un valor felt252
como entrada y llama a parse_u8
. El operador ?
se utiliza para propagar el error, si lo hay, o desenvolver el valor correcto.
Y con un pequeño caso de prueba:
fn parse_u8(s: felt252) -> Result<u8, felt252> {
match s.try_into() {
Option::Some(value) => Result::Ok(value),
Option::None => Result::Err('Invalid integer'),
}
}
fn do_something_with_parse_u8(input: felt252) -> Result<u8, felt252> {
let input_to_u8: u8 = parse_u8(input)?;
// DO SOMETHING
let res = input_to_u8 - 1;
Result::Ok(res)
}
#[cfg(test)]
mod tests {
use super::do_something_with_parse_u8;
use debug::PrintTrait;
#[test]
fn test_function_2() {
let number: felt252 = 258_felt252;
match do_something_with_parse_u8(number) {
Result::Ok(value) => value.print(),
Result::Err(e) => e.print()
}
}
}
La consola mostrará el error "Invalid Integer".
Summary
Vimos que los errores recuperables pueden ser manejados en Cairo usando el enum Result, que tiene dos variantes: Ok
y Err
. El enum Result<T, E>
es genérico, con los tipos T
y E
representando los valores de éxito y error, respectivamente. El ResultTrait
proporciona métodos para trabajar con Result<T, E>
, como desenvolver valores, comprobar si el resultado es Ok
o Err
, y asustar con mensajes personalizados.
Para gestionar errores recuperables, una función puede devolver un tipo Result
y utilizar la concordancia de patrones para gestionar el éxito o el fracaso de una operación. El operador ?
puede utilizarse para gestionar errores implícitamente, propagando el error o desenvolviendo el valor correcto. Esto permite una gestión de errores más concisa y clara, en la que el autor de la llamada es responsable de gestionar los errores generados por la función llamada.