Enums
Enumerators
Define a data type with multiple possible variants.
If Today is Tuesday, it can be written as Tue, Tuesday, TUESDAY, T in several forms. How to standarize it?
Example:
- Days of the Week.
- Months in a year.
- Traffic light colors.
It Enumerates a finite number of options or types.
- Create custom enum types.
- How enums are commonly used.
- There are few standard enums you will use in Rust.
#[derive(Debug)] enum TrafficLight{ Red, Yellow, Green } fn main(){ let my_light = TrafficLight::Red; println!("{:?}", my_light); }
Importance of using derive(Debug) Trait. Execute this script to see the result.
enum TrafficLight{ Red, Yellow, Green } fn main(){ let my_light = TrafficLight::Red; println!("{:?}", my_light); }
In addition to simply representing one of several types, we can have additional data based on the value.
Let's add Unnamed parameters in parenthesis.
// Enum with additional data #[derive(Debug)] enum TrafficLight{ Red (bool), Yellow (bool), Green (bool) } fn main(){ let my_light = TrafficLight::Red(true); println!("{:?}", my_light); }
// Separate the value and enum result #[derive(Debug)] enum TrafficLight{ Red (bool), Yellow (bool), Green (bool) } fn main(){ let my_light = TrafficLight::Red(false); println!("{:?}", my_light); match my_light { TrafficLight::Red(is_active) => println!("Red: {}", is_active), TrafficLight::Yellow(is_active) => println!("Yellow: {}", is_active), TrafficLight::Green(is_active) => println!("Green: {}", is_active), } }
Enums inside Enums
#[derive(Debug)] enum TrafficLight { Red(bool), Yellow(bool), Green(bool), } #[derive(Debug)] enum Vehicle { Stop, Drive(f64), CheckLight(TrafficLight), } fn main() { let my_light = TrafficLight::Red(true); let instruction = Vehicle::CheckLight(my_light); match instruction { Vehicle::Stop => println!("Stop"), Vehicle::Drive(speed) => println!("Drive at speed: {}", speed), Vehicle::CheckLight(light) => match light { TrafficLight::Red(is_active) => println!("Red light: {}", is_active), TrafficLight::Yellow(is_active) => println!("Yellow light: {}", is_active), TrafficLight::Green(is_active) => println!("Green light: {}", is_active), }, } }
In Database world, commonly usages are
enum RightClick {
Copy,
Paste,
Save,
Quit
}
enum FileFormat {
CSV,
Parquet,
Avro,
JSON,
XML,
}
enum LogLevel {
Debug,
Info,
Warn,
Error,
Critical,
}
enum DataTier {
Hot,
Warm,
Cold,
Archived,
}
Enum Implements
Similar to Struct, you can implement an interface in Enum.
The Implement interface can be helpful when we need to implement some business logic tightly coupled with a discriminatory property of a given enum.
// Modified from Source: Barron Stone Git Repository #[derive(Debug)] enum Shape { Circle(f64), // radius Rectangle(f64, f64), // width, height Triangle(f64, f64, f64) // sides a, b, c } impl Shape { fn get_perimeter(&self) -> f64 { match *self { Shape::Circle(r) => r * 2.0 * std::f64::consts::PI, Shape::Rectangle(w, h) => (2.0 * w) + (2.0 * h), Shape::Triangle(a, b, c) => a + b + c } } } fn main() { let my_shape = Shape::Circle(1.2); println!("my_shape is {:?}", my_shape); let perimeter = my_shape.get_perimeter(); println!("perimeter is {}", perimeter); let my_shape1 = Shape::Triangle(3.0,4.0,5.0); println!("my_shape is {:?}", my_shape1); let perimeter1 = my_shape1.get_perimeter(); println!("perimeter is {}", perimeter1); let my_shape2 = Shape::Rectangle(4.0,5.0); println!("my_shape is {:?}", my_shape2); let perimeter2 = my_shape2.get_perimeter(); println!("perimeter is {}", perimeter2); }
Commonly used standard Enums
Option
Many languages use NULL to indicate no value.
Errors often occur when using a NULL.
Rust does not have a traditional null value.
// Sample Option Enum fn main() { let x = Some(5); //Option <i32> let y = Some(4.0); // Option <f64> let z = Some("Hello"); //Option <&str> let a = None; //N should be upper case let a_vec: Option<Vec<i32>> = Some(vec![0, 1, 2, 3]); }
// Simple Division fn try_division(dividend: i32, divisor: i32) -> i32 { dividend / divisor } fn main() { println!("{}",try_division(4, 2)); //println!("{}",try_division(4, 0)); }
Sometimes it's desirable to catch the failure of some parts of a program instead of calling panic!
; this can be accomplished using the Option
enum.
// Common Enum
enum Option <T>{
Some(T),
None
}
This achieves the same concept as a traditional null value, but implementing it in terms of an enum data type compiler can check to make sure you are handling it correctly.
It's commonly used and included in the prelude. That means additional use statements are needed.
// Error Handling // An integer division that doesn't `panic!` fn checked_division(dividend: i32, divisor: i32) -> Option<i32> { if divisor == 0 { // Failure is represented as the `None` variant None } else { // Result is wrapped in a `Some` variant Some(dividend / divisor) } } fn try_division(dividend: i32, divisor: i32) { // `Option` values can be pattern matched, just like other enums match checked_division(dividend, divisor) { None => println!("{} / {} failed!", dividend, divisor), Some(quotient) => { println!("{} / {} = {}", dividend, divisor, quotient) }, } } fn main() { //try_division(4, 2); try_division(4, 0); }
Result
// Another Prelude
enum Result<T, E>{
Ok(T),
Err(E)
}
Ok(T)
It contains the success value.
Err(E)
Contains the error value
// Simple Simple Interest Function fn si(p:f32,n:f32,r:f32) -> f32 { (p * n * r )/100 as f32 } fn main(){ let p:f32 = 10000.00; let n:f32 = 3.00; let r:f32 = 1.4; let si = si(p,n,r); println!("Simple Interest = {si}"); }
How to handle if the parameter is Zero?
// SI with conditions fn si(p:f32,n:f32,r:f32) -> f32 { if p <= 0. { println!("p cannot be zero"); } if n <= 0.{ println!("n cannot be zero"); } if r <= 0. { println!("r cannot be zero"); } (p * n * r )/100 as f32 } fn main(){ let p:f32 = 10000.00; let n:f32 = 3.00; let r:f32 = 0.0; let si = si(p,n,r); println!("Simple Interest = {si}"); }
Do you notice any issues with this?
Yes, the message is displayed, but still, the calculation takes place, and the Error message is only for informational purposes.
How does Result enum help in this case?
// using Result fn si(p: f32, n: f32, r: f32) -> Result<f32, String> { if p <= 0. { return Err("Principal cannot be less or equal to zero".to_string()); } if n <= 0. { return Err("Number of years cannot be less or equal to zero".to_string()); } if r <= 0. { return Err("Rate cannot be less or equal to zero".to_string()); } Ok((p * n * r) / 100.0) } fn main() { let p: f32 = 10000.0; let n: f32 = 3.0; let r: f32 = 1.4; match si(p, n, r) { Ok(result) => println!("si = {result}"), Err(e) => println!("error occured {:?}", e), } }
Difference when using Result and Option Example
// using Result fn si_using_result(p: f32, n: f32, r: f32) -> Result<f32, String> { if p <= 0. { return Err("Principal cannot be less or equal to zero".to_string()); } if n <= 0. { return Err("Number of years cannot be less or equal to zero".to_string()); } if r <= 0. { return Err("Rate cannot be less or equal to zero".to_string()); } Ok((p * n * r) / 100.0) } fn si_using_option(p: f32, n: f32, r: f32) -> Option<f32> { if p <= 0. { return None; } if n <= 0. { return None; } if r <= 0. { return None; } Some((p * n * r) / 100.0) } fn main() { let p: f32 = 10000.0; let n: f32 = 0.0; let r: f32 = 1.4; match si_using_result(p, n, r) { Ok(result) => println!("si = {result}"), Err(e) => println!("Result ENUM - Error occured {:?}", e), } match si_using_option(p, n, r) { Some(result) => println!("si = {result}"), None => println!("Option ENUM - Error occurred: one of the inputs is non-positive"), } }
The ? operator
? is used for Error Propogation
There is an even shorter way to deal with Result (and Option), shorter than a match and even shorter than if let. It is called the "question mark operator."
After a function that returns a result, you can add ?. This will:
If its Ok, return the result If its Err, return the error
Before using ?
fn si_using_result(p: f32, n: f32, r: f32) -> Result<f32, String> { if p <= 0. { return Err("Principal cannot be less or equal to zero".to_string()); } if n <= 0. { return Err("Number of years cannot be less or equal to zero".to_string()); } if r <= 0. { return Err("Rate cannot be less or equal to zero".to_string()); } Ok((p * n * r) / 100.0) } fn main() { let p: f32 = 10000.0; let n: f32 = 0.0; let r: f32 = 1.4; match si_using_result(p, n, r) { Ok(result) => println!("si = {result}"), Err(e) => println!("Result ENUM - Error occured {:?}", e), } }
Now using ?
Returning Ok()
fn si_using_result(p: f32, n: f32, r: f32) -> Result<f32, String> { if p <= 0. { return Err("Principal cannot be less or equal to zero".to_string()); } if n <= 0. { return Err("Number of years cannot be less or equal to zero".to_string()); } if r <= 0. { return Err("Rate cannot be less or equal to zero".to_string()); } Ok((p * n * r) / 100.0) } fn main() -> Result<(), String> { let p: f32 = 10000.0; let n: f32 = 3.0; let r: f32 = 1.4; let result = si_using_result(p, n, r)?; println!("SI calc with {p},{n},{r} = {result}"); Ok(()) }
Returning Err
fn si_using_result(p: f32, n: f32, r: f32) -> Result<f32, String> { if p <= 0. { return Err("Principal cannot be less or equal to zero".to_string()); } if n <= 0. { return Err("Number of years cannot be less or equal to zero".to_string()); } if r <= 0. { return Err("Rate cannot be less or equal to zero".to_string()); } Ok((p * n * r) / 100.0) } fn main() -> Result<(), String> { let p: f32 = 10000.0; let n: f32 = 0.0; let r: f32 = 1.4; let result = si_using_result(p, n, r)?; println!("SI calc with {p},{n},{r} = {result}"); Ok(()) }
Another way to write using a intermediate function
fn calc_si(p: f32, n: f32, r: f32) -> Result<f32, String> { if p <= 0. { return Err("Principal cannot be less or equal to zero".to_string()); } if n <= 0. { return Err("Number of years cannot be less or equal to zero".to_string()); } if r <= 0. { return Err("Rate cannot be less or equal to zero".to_string()); } Ok((p * n * r) / 100.0) } fn print_si(p: f32, n: f32, r: f32) -> Result<f32, String>{ let result_si = calc_si(p,n,r)?; Ok(result_si) } fn main() { let p: f32 = 10000.0; let n: f32 = 3.; let r: f32 = 1.4; println!("{:?}",print_si(p,n,r)); }
Loop through and convert Integers and return Error message for others
// Simple Parse Example fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> { let parsed_number = input.parse::<i32>()?; Ok(parsed_number) } fn main() { let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"]; for item in str_vec { let parsed = parse_str(item); println!("{:?}", parsed); } }
unwrap
is a method available on Option and Result types that is used to extract the contained value. If the Option is Some or the Result is Ok, unwrap returns the contained value. However, if the Option is None or the Result is Err, unwrap will cause the program to panic and terminate execution.
• Use unwrap sparingly and only when you are sure it won’t cause a panic. • Prefer expect with a meaningful message for clarity in cases where you are sure the operation won’t fail.
fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> { let parsed_number = input.parse::<i32>()?; Ok(parsed_number) } fn main() { let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"]; for item in str_vec { let parsed = parse_str(item).unwrap(); println!("{:?}", parsed); } }
unwrap_or_else
This can be used in production code.
fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> { let parsed_number = input.parse::<i32>()?; Ok(parsed_number) } fn main() { let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"]; for item in str_vec { let parsed = parse_str(item).unwrap_or_else(|err| { println!("Error: failed to parse '{}'. Reason: {}", item, err); -1 // Provide a fallback value }); if parsed != -1 { println!("{}", parsed); } } }