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

With Crystal, I just feels like copy paste from Ruby code.

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

record Song,
  title : String = "", 
  tags : Array(String) = [] of String

def songs
  [
    (Song.new "Cantaloupe Island",
             ["60s", "jazz"]),
    (Song.new "Let It Be",
             ["60s", "rock"]),
    (Song.new "Knockin' on Heaven's Door",
             ["70s", "rock"]),
    (Song.new "Emotion",
             ["70s", "pop"]),
    (Song.new "The River"),
  ]
end

Crystal Solution

The Answer

There might be many ways to do things in Crystal. One of them is this oneliner as below:

puts songs
  .map { |song| song.tags }
  .select { |tags| tags != [] of String }
  .flatten.uniq

Enough with introduction, at this point we should go straight to coding.

Environment

No need any special setup. Just run and voila..!


1: Data Structure

We are going to use array throught out this article.

Simple Array

Before building a struct, I begin with simple array.

tags: Array(String) = ["rock", "jazz", "rock", "pop", "pop"]

puts tags

It is easy to dump variable in crystal using puts. With the result similar as below array:

$ crystal 01-tags.cr
["rock", "jazz", "rock", "pop", "pop"]

The Song Record

We can continue our journey to records. This record is actually just a macro.

record Song,
  title : String = "", 
  tags : Array(String) = [] of String

song = Song.new "Cantaloupe Island",
                ["60s", "jazz"]

puts song

With the result similar as below record:

$ crystal 02-song.cr
Song(@title="Cantaloupe Island", @tags=["60s", "jazz"])

Crystal: The Song Record Macro

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.

record Song,
  title : String = "", 
  tags : Array(String) = [] of String

songs: Array(Song) = [
  (Song.new "Cantaloupe Island",
           ["60s", "jazz"]),
  (Song.new "Let It Be",
           ["60s", "rock"]),
  (Song.new "Knockin' on Heaven's Door",
           ["70s", "rock"]),
  (Song.new "Emotion",
           ["70s", "pop"]),
  (Song.new "The River"),
]

songs.each { |song| puts song }

It is more convenience to show the result using each, with the result similar as below sequential lines of Song type:

❯ crystal 03-songs.cr
Song(@title="Cantaloupe Island", @tags=["60s", "jazz"])
Song(@title="Let It Be", @tags=["60s", "rock"])
Song(@title="Knockin' on Heaven's Door", @tags=["70s", "rock"])
Song(@title="Emotion", @tags=["70s", "pop"])
Song(@title="The River", @tags=[])

Instead of {…} syntax, we can utilize do … end block.

Maybe Null

To avoid null value, we simply use default value instead.

record Song,
  title : String = "", 
  tags : Array(String) = [] of String

For empty element, we have to write the complete type such as [] of String.


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

The code can be shown as below:

record Song,
  title : String = "", 
  tags : Array(String) = [] of String

def songs
  [
    (Song.new "Cantaloupe Island",
             ["60s", "jazz"]),
    (Song.new "Let It Be",
             ["60s", "rock"]),
    (Song.new "Knockin' on Heaven's Door",
             ["70s", "rock"]),
    (Song.new "Emotion",
             ["70s", "pop"]),
    (Song.new "The River"),
  ]
end

Crystal: The Songs Module Containing Method

Using Songs Module

Now we can have a very short code.

require "./my-songs"
songs.each { |song| puts song }

With the result exactly the same as above array.

Crystal: Using Songs Module


3: Finishing The Task

Map, Select, Compact, Flatten, Uniq

Extracting Fields

We can use map to extract tags field. And then select to filter out record with empty tags.

require "./my-songs"

tagss = songs
  .map { |song| song.tags }
  .select { |tags| tags != [] of String } 

puts tagss

With the result similar as below array of array:

$ crystal 05-filter.cr
[["60s", "jazz"], ["60s", "rock"], ["70s", "rock"], ["70s", "pop"]]

We can combine the map and filter with this compact_map method:

tagss = songs.compact_map do |song|
  song.tags  if song.tags != [] of String
end 

With exactly the same result.

Flatten

There is already a standard library for flatten, so we can write as below:

puts songs
  .map { |song| song.tags }
  .select { |tags| tags != [] of String }
  .flatten

Unique

In order to get to get distinct values, we can utilize uniq from standard library.

require "./my-songs"

puts songs
  .map { |song| song.tags }
  .select { |tags| tags != [] of String }
  .flatten
  .uniq

With the final result similar as below array:

$ crystal 06-unique.cr
["60s", "jazz", "rock", "70s", "pop"]

Crystal: Finishing The Task

Now the code is very clear. We can understand exactly what it does, by just reading the code.


4: Concurrency with Channel using Fiber

Concurrency in crystal, can be handled through channel using fiber. We are going to build show case to pass data between thread, using a custom flatten function.

Reference

Custom Flatten Function

In order to make a Sender demo, I need to make custom handmade flatten function. This is simply flatten using all tags from all songs records, using nested each iterator.

require "./my-songs"

def flatten()
  tags = [] of String
  
  songs.each { |song| 
    if song.tags != [] of String
      song.tags.each { |tag| tags << tag }
   end  
  }
  
  tags
end

puts flatten()

With the result similar as below vector:

$ crystal 07-flatten.cr
["60s", "jazz", "60s", "rock", "70s", "rock", "70s", "pop"]

Crystal: Custom Flatten Function in Imperative Fashioned

We are going to utilize this imperative fashioned as a show case, to pass data between spawn using channel. This way we can send the result through a channel using fiber, instead of directly pushing the result to array.

Sender and Receiver

We should prepare two functions, one as a sender, and the other one as a receiver.

The first one sending each results through channel.

def sender(channel)
  songs.each do |song| 
    if song.tags != [] of String
      song.tags.each { |tag| channel.send(tag) }
    end  
  end
  channel.send(nil)
end

And then receiver from channel, all the result would be gathered as array.

def receiver(channel)
  tags = [] of String

  while true
    message = channel.receive
    if message != nil
      tags << message.to_s
    else
      puts tags
      break
    end  
  end
end

Crystal Concurrency: Sender and Receiver

Spawn Both Fiber

Pretty short right!

We require a channel with union type, so we can send a nil a stop signal.

require "./my-songs"
channel = Channel(String | Nil).new
spawn sender(channel)
spawn receiver(channel)
Fiber.yield

With the result similar as below array:

$ crystal 08-channel.cr
["60s", "jazz", "60s", "rock", "70s", "rock", "70s", "pop"]

Crystal Concurrency: Spawn Channel using Fiber

We are done with simple concurreny example. Concurency is not so complex after all.


What is Next 🤔?

Consider continue reading [ Go - Playing with Records - Part One ].