cli  
 
Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Where to Discuss?

Local Group

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"])

Rust: Showing The Song Record Structure

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 }
    ]
}

Rust: The Songs Module Containing Get Songs Method

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.

Rust: Using Songs Module


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"]

Rust: Extracting Fields with Filtermap

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"]

Rust: Finishing The Task: Unique Tag Elements


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 ].