Preface
Goal: A practical case to collect unique record fields using Rust.
To code in Rust
, we need to understand,
the concept of ownership
and borrowing.
At first we have to fight with the compiler,
but in the end we have memory safe executable.
Reference Reading
Source Examples
You can obtain source examples here:
Common Use Case
Task: Get the unique tag string
Please read overview for more detail.
Prepopulated Data
Songs and Poetry
use std::fmt;
pub struct Song<'lt> {
title: &'lt str,
pub tags : Option<Vec<&'lt str>>
}
pub fn get_songs<'lt>() -> Vec<Song<'lt>> {
vec![
Song { title: "Cantaloupe Island",
tags: Some(vec!["60s", "jazz"])},
Song { title: "Let It Be",
tags: Some(vec!["60s", "rock"])},
Song { title: "Knockin' on Heaven's Door",
tags: Some(vec!["70s", "rock"])},
Song { title: "Emotion",
tags: Some(vec!["70s", "pop"])},
Song { title: "The River", tags: None }
]
}
Rust Solution
The Answer
There might be many ways to do things in Rust
.
One of them is this oneliner as below:
fn main() {
let songs: Vec<my_songs::Song> =
my_songs::get_songs();
let tags: Vec<&str> = songs
.into_iter()
.filter_map(|song| song.tags)
.flat_map(|tag_s| tag_s)
.collect();
let hashtags: HashSet<&str> =
HashSet::from_iter(tags.iter().cloned());
println!("{:?}", hashtags);
}
Enough with introduction, at this point we should rust straight to coding.
Environment
No need any special setup.
I do not use Cargo.
But just the simple rustc
instead.
1: Data Structure
We are going to use vector
throught out this article.
vector
has dynamic size, in contrast with fix length array.
Array of Literal String
Before building a struct
,
I begin with simple array
with fix length [&str; 5]
as below code:
fn main() {
let tags: [&str; 5] =
["rock", "jazz", "rock", "pop", "pop"];
println!("{:?}", tags);
}
We can dump variable in rust
using {:?}
.
With the result similar as below array
:
$ rustc 01-tags-debug.rs
$ ./01-tags-debug
["rock", "jazz", "rock", "pop", "pop"]
The Song Struct
We can continue our journey to records, using struct
.
The idea is just wrting down the struct as below.
struct Song {
title: &str,
tags : Vec<&str>
}
But this won’t compiled.
Because this struct has borrowing &
operator.
So instead we should declare a lifetime as below:
struct Song<'lt> {
title: &'lt str,
tags : Vec<&'lt str>
}
I know its weird 😃, but somehow I like it.
Using Struct
The complete code is as below:
struct Song<'lt> {
title: &'lt str,
tags : Vec<&'lt str>
}
fn main() {
let song = Song {
title: "Cantaloupe Island",
tags: vec!["60s", "jazz"]
};
println!("| {} - {:?}|", &song.title, &song.tags);
}
With the result similar as below record:
$ rustc 02-song-lifetime.rs
$ ./02-song-lifetime
| Cantaloupe Island - ["60s", "jazz"]|
Format in Showing Struct
We can set our own custom format for this struct Since the struct have liftime declaration, the display format should also equipped with lifetime declaration.
use std::fmt;
impl<'lt> fmt::Show for Song<'lt> {
fn fmt(&self, f: &mut fmt::Formatter)-> fmt::Result {
write!(f, "({}, {:?})", self.title, self.tags)
}
}
Now we can write the code shorter as shown below:
fn main() {
let song = Song {
title: "Cantaloupe Island",
tags: vec!["60s", "jazz"]
};
println!("{}", &song);
}
With the result similar as below record:
$ rustc 02-song-fmt.rs
$ ./02-song-fmt
(Cantaloupe Island, ["60s", "jazz"])
Array of Song Record
Meet The Songs Array
From just karaoke, we can go pro with recording studio. From simple data, we can build a structure to solve our task.
use std::fmt;
struct Song<'lt> {
title: &'lt str,
tags : Vec<&'lt str>
}
impl<'lt> fmt::Show for Song<'lt> {
fn fmt(&self, f: &mut fmt::Formatter)-> fmt::Result {
write!(f, "({}, {:?})", self.title, self.tags)
}
}
fn main() {
let songs: [Song; 5] = [
Song { title: "Cantaloupe Island",
tags: vec!["60s", "jazz"]},
Song { title: "Let It Be",
tags: vec!["60s", "rock"]},
Song { title: "Knockin' on Heaven's Door",
tags: vec!["70s", "rock"]},
Song { title: "Emotion",
tags: vec!["70s", "pop"]},
Song { title: "The River", tags: vec![] }
];
for song in songs.iter() {
println!("{}", song);
}
}
It is more convenience to show the result using for in .iter
,
with the result similar as below sequential lines of Song
struct:
$ rustc 03-songs-array.rs
$ ./03-songs-array
(Cantaloupe Island, ["60s", "jazz"])
(Let It Be, ["60s", "rock"])
(Knockin' on Heaven's Door, ["70s", "rock"])
(Emotion, ["70s", "pop"])
(The River, [])
Maybe Null
Wrapping in Options for Nullability
We need, a not too simple, case example.
I also add Option
so tags
can accept None
value,
instead of just empty list.
tags : Option<Vec<&'lt str>>
Now we can rewrite the last record above as below:
Song { title: "The River", tags: None }
This reminds me of the oldschool OCaml
approach.
Get Songs Function
Instead of using immutable let
,
we can define the songs as function return.
fn get_songs<'lt>() -> Vec<Song<'lt>> {
vec![
Song { title: "Cantaloupe Island",
tags: Some(vec!["60s", "jazz"])},
…
]
}
Since we wrapped in Option
,
we should write the data as Some(…)
.
The complete code is as below:
use std::fmt;
struct Song<'lt> {
title: &'lt str,
tags : Option<Vec<&'lt str>>
}
impl<'lt> fmt::Show for Song<'lt> {
fn fmt(&self, f: &mut fmt::Formatter)-> fmt::Result {
write!(f, "({}, {:?})", self.title, self.tags)
}
}
fn get_songs<'lt>() -> Vec<Song<'lt>> {
vec![
Song { title: "Cantaloupe Island",
tags: Some(vec!["60s", "jazz"])},
Song { title: "Let It Be",
tags: Some(vec!["60s", "rock"])},
Song { title: "Knockin' on Heaven's Door",
tags: Some(vec!["70s", "rock"])},
Song { title: "Emotion",
tags: Some(vec!["70s", "pop"])},
Song { title: "The River", tags: None }
]
}
fn main() {
let songs: Vec<Song> = get_songs();
for song in songs.iter() {
println!("{}", song);
}
}
With the result as below list:
$ rustc 03-songs-option.rs
$ ./03-songs-option
(Cantaloupe Island, Some(["60s", "jazz"]))
(Let It Be, Some(["60s", "rock"]))
(Knockin' on Heaven's Door, Some(["70s", "rock"]))
(Emotion, Some(["70s", "pop"]))
(The River, None)
2: Separating Module
Since we need to reuse the songs record multiple times, it is a good idea to separate the record from logic.
Songs Module
It is so simple, really. You just need to cut and paste the cod, to other file in the same directory. The code can be shown as below:
use std::fmt;
pub struct Song<'lt> {
title: &'lt str,
pub tags : Option<Vec<&'lt str>>
}
impl<'lt> fmt::Show for Song<'lt> {
fn fmt(&self, f: &mut fmt::Formatter)-> fmt::Result {
write!(f, "({}, {:?})", self.title, self.tags)
}
}
pub fn get_songs<'lt>() -> Vec<Song<'lt>> {
vec![
Song { title: "Cantaloupe Island",
tags: Some(vec!["60s", "jazz"])},
Song { title: "Let It Be",
tags: Some(vec!["60s", "rock"])},
Song { title: "Knockin' on Heaven's Door",
tags: Some(vec!["70s", "rock"])},
Song { title: "Emotion",
tags: Some(vec!["70s", "pop"])},
Song { title: "The River", tags: None }
]
}
Using Songs Module
Just add mod my_songs;
.
Now we can have a very short code.
mod my_songs;
fn main() {
let songs: Vec<my_songs::Song> = my_songs::get_songs();
for song in songs.iter() {
println!("{}", song);
}
}
With the result exactly the same as above array.
3: Extracting Fields
Map, Filter, Unwrap, Filter Map
Map
We can start extracting field by using map
.
mod my_songs;
fn main() {
let songs: Vec<my_songs::Song> = my_songs::get_songs();
let tags_os: Vec<Option<Vec<&str>>> =
songs.into_iter().map(|song| song.tags).collect();
for tags_o in tags_os.iter() {
println!("{:?}", tags_o)
}
}
With the result similar as below sequential lines of tags option
:
$ rustc 05-map.rs
$ ./05-map
Some(["60s", "jazz"])
Some(["60s", "rock"])
Some(["70s", "rock"])
Some(["70s", "pop"])
None
Although the type signature looks so complex: Vec<Option<Vec<&str>>>
,
It is make sense when we compare with the output.
Filter
We can also filter out the None
row element,
in chained function fashion.
mod my_songs;
fn main() {
let songs: Vec<my_songs::Song> = my_songs::get_songs();
let tags_os: Vec<Option<Vec<&str>>> = songs
.into_iter()
.map(|song| song.tags)
.filter(|tags| tags.is_some())
.collect();
for tags_o in tags_os.iter() {
println!("{:?}", tags_o)
}
}
With the result similar as below sequential lines of tags option
:
$ rustc 05-filter.rs
$ ./05-filter
Some(["60s", "jazz"])
Some(["60s", "rock"])
Some(["70s", "rock"])
Some(["70s", "pop"])
Unwrap
And so on with chaining function.
We can unwrap
the Option
in map
.
mod my_songs;
fn main() {
let songs: Vec<my_songs::Song> = my_songs::get_songs();
let tags_s: Vec<Vec<&str>> = songs
.into_iter()
.map(|song| song.tags)
.filter(|tags| tags.is_some())
.map(|tags| tags.unwrap())
.collect();
for tags in tags_s.iter() {
println!("{:?}", tags)
}
}
With the result similar as below sequential lines of tags
:
$ rustc 05-unwrap.rs
$ ./05-unwrap
["60s", "jazz"]
["60s", "rock"]
["70s", "rock"]
["70s", "pop"]
Beware that this time,
the type signature has changed to Vec<Vec<&str>>
.
Filter Map
We actually just require one function to do that all operation.
mod my_songs;
fn main() {
let songs: Vec<my_songs::Song> = my_songs::get_songs();
let tags_s: Vec<Vec<&str>> = songs
.into_iter()
.filter_map(|song| song.tags)
.collect();
for tags in tags_s.iter() {
println!("{:?}", tags)
}
}
With the result similar as below sequential lines of tags option
:
$ rustc 05-filtermap.rs
$ ./05-filtermap
["60s", "jazz"]
["60s", "rock"]
["70s", "rock"]
["70s", "pop"]
The type signature is also Vec<Vec<&str>>
.
4: Finishing The Task
Flatten, HashSet, Unique Function
Flatten
Luckily we can just use flat_map
function.
mod my_songs;
fn main() {
let songs: Vec<my_songs::Song> =
my_songs::get_songs();
let tags: Vec<&str> = songs
.into_iter()
.filter_map(|song| song.tags)
.flat_map(|tag_s| tag_s)
.collect();
println!("{:?}", tags)
}
With the result similar as below literal string vector
:
$ rustc 06-flatten.rs
$ ./06-flatten
["60s", "jazz", "60s", "rock", "70s", "rock", "70s", "pop"]
HashSet
There are different alternative, in order to get unique value.
One common solution is to utilize HashSet
.
mod my_songs;
use std::collections::HashSet;
use std::iter::FromIterator;
fn main() {
let songs: Vec<my_songs::Song> =
my_songs::get_songs();
let tags: Vec<&str> = songs
.into_iter()
.filter_map(|song| song.tags)
.flat_map(|tag_s| tag_s)
.collect();
let hashtags: HashSet<&str> =
HashSet::from_iter(tags.iter().cloned());
println!("{:?}", hashtags)
}
With the result similar as below literal string hash set
:
$ rustc 06-hashset.rs
$ ./06-hashset
{"60s", "jazz", "rock", "70s", "pop"}
Unique Function
mod my_songs;
use std::collections::HashSet;
use std::iter::FromIterator;
fn unique(tags: Vec<&str>) -> Vec<&str> {
let hashtags: HashSet<&str> =
HashSet::from_iter(tags.iter().cloned());
Vec::from_iter(hashtags.iter().cloned())
}
fn main() {
let songs: Vec<my_songs::Song> =
my_songs::get_songs();
let tags: Vec<&str> = songs
.into_iter()
.filter_map(|song| song.tags)
.flat_map(|tag_s| tag_s)
.collect();
println!("{:?}", unique(tags))
}
With the result similar as below literal string vector
:
$ rustc 06-fn-unique.rs
$ ./06-fn-unique
["70s", "pop", "60s", "rock", "jazz"]
What is Next 🤔?
We are done with basic example. But there is another way to do all the above material. A simpler struct alternative, and custom unique algorithm.
Consider continue reading [ Rust - Playing with Records - Part Two ].