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"])
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
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.
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"]
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 tag
s 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"]
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
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"]
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 ].