Rust enumeration, matching, and options APIS
afterCorbin Crutchley
April 16, 2021
1350 words
Originally published on coderpad.io
If you’ve been active in the programming community over the past few years, you’ve no doubt heard of Rust. Its technical base and vibrant community have proven themselves to be a good benchmark for rapid language growth.
But what did Rust do to generate such a positive reaction from the community? Rust not only provides a great deal of memory security, which was rare in low-level languages of the past, but it also includes powerful features that make development better.
One of the many features that highlight Rust’s capabilities is its handling of enumerations and matching.
The enumeration
Like many languages with strict typing, Rust has an enum feature. Declaring an enumeration is simple, starting with the pub enum value and naming it.
pub enum CodeLang {
Rust,
JavaScript,
Swift,
Kotlin,
// ...
}
Copy the code
To create a variable with this enumerated type, you can use the enumeration’s name and value:
fn main() {
let lang = CodeLang::Rust;
}
Copy the code
Similarly, you can use enum as a type in places like function arguments. Suppose you want to test which version of the programming language CoderPad supports. We’ll start by hard-coding the Rust version:
fn get_version(_lang: CodeLang) -> &'static str {
return "1.46";
}
Copy the code
While this code works, it’s not very powerful. If “CodeLang::JavaScript” is passed in, the version number is incorrect. Let’s see how to solve this problem in the next section.
matching
Although you could use the if statement to detect the passed enumeration, as follows:
fn get_version(lang: CodeLang) -> &'static str {
if let CodeLang::Rust = lang {
return "1.46";
}
if let CodeLang::JavaScript = lang {
return "2021";
}
return ""
}
fn main() {
let lang = CodeLang::Rust;
let ver = get_version(lang);
println!("Version {}", ver);
}
Copy the code
This can easily become unwieldy when dealing with more than one or two values in an enumeration. This is where Rust’s match operator comes in. Let’s match the variable with all the existing values in the enumeration:
fn get_version(lang: CodeLang) -> &'static str {
match lang {
CodeLang::Rust => "1.46",
CodeLang::JavaScript => "2021",
CodeLang::Swift => "5.3",
CodeLang::Python => "3.8"}}Copy the code
If you’re familiar with programming languages that have functionality similar to “Switch/Case,” this example is an approximation of that functionality. However, as you’ll soon see, matchRust is much more powerful than most Switch/Case implementations.
Pattern matching
While most implementations of Switch/Case only allow simple primitive matches, such as strings or numbers, Rustmatch allows you to have more fine-grained control over what and how matches are made. For example, you can use the _ identifier to match anything that doesn’t match:
fn get_version(lang: CodeLang) -> &'static str {
match lang {
CodeLang::Rust => "1.46", _ = >"Unknown version"}}Copy the code
You can also match multiple values at once. In this example, we are checking versions of more than one programming language at a time.
fn get_version<'a>(lang: CodeLang, other_lang: CodeLang) -> (&'a str, &'a str) {
match (lang, other_lang) {
(CodeLang::Rust, CodeLang::Python) => ("1.46"."3.8"), _ = > ("Unknown"."Unknown")}}Copy the code
This shows match. However, you can do more with enumerations.
Value is stored
Not only the enumeration values themselves, but you can also store values in the enumeration for later access.
For example, CoderPad supports two different versions of Python. However, instead of creating aCodeLang::Python and CoderLang::Python2enum values, we can use a value and store the major version in it.
pub enum CodeLang {
Rust,
JavaScript,
Swift,
Python(u8),
// ...
}
fn main() {
let python2 = CodeLang::Python(2);
let pythonVer = get_version(python2);
}
Copy the code
We can extend our expression from the previous if let to access the values in it:
if let CodeLang::Python(ver) = python2 {
println!("Python version is {}", ver);
}
Copy the code
However, as before, we can use match to unpack the values in the enumeration:
fn get_version(lang: CodeLang) -> &'static str {
match lang {
CodeLang::Rust => "1.46",
CodeLang::JavaScript => "2021",
CodeLang::Python(ver) => {
if ver == 3 { "3.8" } else { "2.7" }
},
_ => "Unknown"}}Copy the code
However, not all enums need to be set up manually! Rust has several enumerations built into the language that you can use at any time.
Options for the enumeration
Although we currently return the string “Unknown” as a version, this is not ideal. That is, we have to do a string comparison to check if we are returning a known version, rather than using a value that is specific to missing values.
This is where Rust’s Option enumeration comes in. Option
describes a data type that has Some(data) or None.
For example, we could rewrite the above function as:
fn get_version<'a>(lang: CodeLang) -> OptionThe < &'a str> {
match lang {
CodeLang::Rust => Some("1.46"),
CodeLang::JavaScript => Some("2021"),
CodeLang::Python(ver) => {
if ver == 3 { Some("3.8")}else { Some("2.7"}}, _ =>None}}Copy the code
By doing so, we can make our logic more representative and check if a value is None
fn main() {
let swift_version = get_version(CodeLang::Swift);
if let None = swift_version {
println!("We could not find a valid version of your tool");
return; }}Copy the code
Finally, we can of course use match to migrate if from an to check when the value is set:
fn main() {
let code_version = get_version(CodeLang::Rust);
match code_version {
Some(val) => {
println!("Your version is {}", val);
},
None= > {println!("We could not find a valid version of your tool");
return; }}}Copy the code
Operator,
While the code above works as expected, if we add more conditional logic, we might find ourselves wanting to abstract. Let’s look at some of the abstractions Rust provides us
Map operator
What if we want to convert rust_version to a string, but want to handle the existing edge case None?
You might write something like:
fn main() {
let rust_version = get_version(CodeLang::Rust);
let version_str = match rust_version {
Some(val) => {
Some(format!("Your version is {}", val))
},
None= >None
};
if let Some(val) = version_str {
println!("{}", val);
return; }}Copy the code
This match takes Some and maps it to a new value and lets Nones resolve to None is still baked into the Option enumeration method called map:
fn main() {
let rust_version = get_version(CodeLang::Rust);
let version_str = rust_version.map(|val| {
format!("Your version is {}", val)
});
if let Some(val) = version_str {
println!("{}", val);
return; }}Copy the code
How close is the implementation of.map to what we’ve done before? Let’s look at the source code implementation of Rust.
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> {
match self {
Some(x) => Some(f(x)),
None= >None,}}Copy the code
As you can see, we match our implementation very similarly, matching Some to another Some and None and None
And then the operator
While it is useful in most cases to automatically wrap the return value of the.map function into aSome, there are times when you may want to conditionally place the value in the map
Suppose we only want the version number that contains a dot (indicating that there is a minor version). We can do this:
fn main() {
let rust_version = get_version(CodeLang::JavaScript);
let version_str = match rust_version {
Some(val) => {
if val.contains(".") {
Some(format!("Your version is {}", val))
} else {
None}},None= >None
};
if let Some(val) = version_str {
println!("{}", val);
return; }}Copy the code
We can override it using Rust’s and_THEN operator:
fn main() {
let rust_version = get_version(CodeLang::JavaScript);
let version_str = rust_version.and_then(|val| {
if val.contains(".") {
Some(format!("Your version is {}", val))
} else {
None}});if let Some(val) = version_str {
println!("{}", val);
return; }}Copy the code
If we look at the operator rust source code, we can see the similar.map implementation, just Some without wrapping fn:
pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U> {
match self {
Some(x) => f(x),
None= >None,}}Copy the code
Put it all together
Now that we’re familiar with Option enumerations, operators, and pattern matching, let’s put them all together!
Let’s start with the same function baseline get_version used for several examples:
use regex::Regex;
pub enum CodeLang {
Rust,
JavaScript,
Swift,
Python(u8),
// ...
}
fn get_version<'a>(lang: CodeLang) -> OptionThe < &'a str> {
match lang {
CodeLang::Rust => Some("1.46"),
CodeLang::JavaScript => Some("2021"),
CodeLang::Python(ver) => {
if ver == 3 { Some("3.8")}else { Some("2.7"}}, _ =>None}}fn main() {
let lang = CodeLang::JavaScript;
let lang_version = get_version(lang);
}
Copy the code
Given this baseline, let’s build a Semver inspector. Given a coding language, tell us what the major and minor versions of the language are.
For example, Rust (1.46) returns “Major: 1.minor: 46”, while JavaScript (2021) returns ** “Major: 2021. Minor: 0**”
We will perform this check using a regular expression that resolves any point in the version string.
(\d+)(? :\.(\d+))?Copy the code
This regular expression matches the first capture group as anything before the first period, and optionally provides a second capture (if there is a period) that matches anything after that period. Let’s add regular expressions and capture to our main function:
let version_regex = Regex::new(r"(\d+)(? :\.(\d+))?").unwrap();
let version_matches = lang_version.and_then(|version_str| {
return version_regex.captures(version_str);
});
Copy the code
In the code example above, we use and_then to flatten captures into a single layer Option enumeration – treating Option as lang_version itself and captures returning an Option.
While.captures sounds like it should return an array of captured strings, it actually returns a structure with various methods and attributes. To get a string for each value, we’ll use version_matches. Map to get the two capture group strings:
let major_minor_captures = version_matches
.map(|caps| {
(
caps.get(1).map(|m| m.as_str()),
caps.get(2).map(|m| m.as_str()),
)
});
Copy the code
While we expect capture group 1 to always provide a value (given our input), if there is no period (such as JavaScript version number “2021”), we see “None” returned in capture group 2. Therefore, caps. Get (2) may be None in some cases. Therefore, we want to make sure that 0 gets aNone at position and converts it to Some<& STR >, Option<& STR > to Some<& STR, & STR >. To do this, we’ll use and_then and a match:
let major_minor = major_minor_captures
.and_then(|(first_opt, second_opt)| {
match (first_opt, second_opt) {
(Some(major), Some(minor)) => Some((major, minor)),
(Some(major), None) = >Some((major, "0")), _ = >None,}});Copy the code
Finally, we can use anif let to deconstruct the values and print the major and minor versions:
if let Some((first, second)) = major_minor {
println!("Major: {}. Minor: {}", first, second);
}
Copy the code
The final version of the project should look like this:
use regex::Regex;
pub enum CodeLang {
Rust,
JavaScript,
Swift,
Python(u8),
// ...
}
fn get_version<'a>(lang: CodeLang) -> OptionThe < &'a str> {
match lang {
CodeLang::Rust => Some("1.46"),
CodeLang::JavaScript => Some("2021"),
CodeLang::Python(ver) => {
if ver == 3 { Some("3.8")}else { Some("2.7"}}, _ =>None}}fn main() {
let lang = CodeLang::JavaScript;
let lang_version = get_version(lang);
let version_regex = Regex::new(r"(\d+)(? :\.(\d+))?").unwrap();
let version_matches = lang_version.and_then(|version_str| {
return version_regex.captures(version_str);
});
let major_minor_captures = version_matches
.map(|caps| {
(
caps.get(1).map(|m| m.as_str()),
caps.get(2).map(|m| m.as_str()),
)
});
let major_minor = major_minor_captures
.and_then(|(first_opt, second_opt)| {
match (first_opt, second_opt) {
(Some(major), Some(minor)) => Some((major, minor)),
(Some(major), None) = >Some((major, "0")), _ = >None,}});if let Some((first, second)) = major_minor {
println!("Major: {}. Minor: {}", first, second); }}Copy the code
Conclusions and Challenges
All of these features are used frequently in Rust applications: enumerations, matches, option operators. We hope you can take advantage of these features as you learn Rust and use them in your applications.
Let’s end with a challenge. If you encounter any questions along the way or have any comments/questions about this article, you can join our public chat community where we discuss general coding topics as well as interviews.
Suppose we track a “patched” version of the software. We want to extend our code logic to support checking for “5.1.2” and returning “2” as the “patched” version. Given a modified regular expression to support three optional capture groups:
(\d+)(? :\.(\d+))? (? :\.(\d+))?Copy the code
How do you modify the following code to support the correctly listed matching versions?
You know your code is running when you can output the following:
Major: 2021. Minor: 0, Patch: 0
Major: 1. Minor: 46, Patch: 0
Major: 5. Minor: 1, Patch: 2
Copy the code