Preface
Goal: A practical case to collect unique record fields using Nim.
Nim
is surprisingly very easy, and also fun to work with.
It is interesting, that Nim
is using macro as syntatic sugar.
But I still can’t predict yet how far the impact will go.
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
type
StringSeq* = seq[string]
Song* = tuple[title: string, tags: StringSeq]
let songs*: seq[Song] = @[
( title: "Cantaloupe Island",
tags : @["60s", "jazz"]),
( title: "Let It Be",
tags : @["60s", "rock"]),
( title: "Knockin' on Heaven's Door",
tags : @["70s", "rock"]),
( title: "Emotion",
tags : @["70s", "pop"]),
( title: "The River",
tags : @[]),
]
Nim Solution
The Answer
There might be many ways to do things in Nim
.
One of them is this oneliner as below:
import sequtils, sets, MySongs
echo songs.mapIt(it.tags).foldl(a & b).toHashSet.toSeq
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 sequence
throught out this article.
Simple Array
Before building a struct
,
I begin with simple array
.
var tags: array[5, string] =
["rock", "jazz", "rock", "pop", "pop"]
echo tags
It is easy to dump variable in nim
using echo
.
With the result similar as below array
:
$ nim compile --run t01_tags
Hint: used config file '/etc/nim/nim.cfg' [Conf]
Hint: used config file '/etc/nim/config.nims' [Conf]
Hint: 7849 lines; 0.038s; 6.996MiB peakmem; Debug build; proj: /home/epsi/Documents/songs/nim/t01_tags; out: /home/epsi/Documents/songs/nim/t01_tags [SuccessX]
Hint: /home/epsi/Documents/songs/nim/t01_tags [Exec]
["rock", "jazz", "rock", "pop", "pop"]
You can also run nim without making executable:
$ nim r t01_tags
Hints
There is a purpose of showing hint
.
But if you wish, it would be clearer if we surpress the hints.
$ nim compile --run --hints:off t01_tags.nim
["rock", "jazz", "rock", "pop", "pop"]
Or set it in nim.cfg
❯ cat /etc/nim/nim.cfg | grep hint
hint[LineTooLong]=off
#hint[XDeclaredButNotUsed]=off
hints=off
Now we can run the compilation quietly.
$ nim r t01_tags.nim
["rock", "jazz", "rock", "pop", "pop"]
The Song Tuple Type
We can continue our journey to records using type
and tuple
.
Since we don’t know the length of the array
,
we use sequence
instead.
type
StringSeq = seq[string]
Song = tuple[title: string, tags: StringSeq]
let song: Song = (
title: "Cantaloupe Island",
tags : @["60s", "jazz"]
)
echo song
With the result similar as below record:
$ nim r t02_song.nim
(title: "Cantaloupe Island", tags: @["60s", "jazz"])
Array of Song Struct
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.
type
StringSeq = seq[string]
Song = tuple[title: string, tags: StringSeq]
let songs: seq[Song] = @[
( title: "Cantaloupe Island",
tags : @["60s", "jazz"]),
( title: "Let It Be",
tags : @["60s", "rock"]),
( title: "Knockin' on Heaven's Door",
tags : @["70s", "rock"]),
( title: "Emotion",
tags : @["70s", "pop"]),
( title: "The River",
tags : @[]),
]
for i, song in songs: writeLine(stdout, song)
It is more convenience to show the result using for
,
with the result similar as below sequential lines of Song
type:
$ nim r t03_songs.nim
(title: "Cantaloupe Island", tags: @["60s", "jazz"])
(title: "Let It Be", tags: @["60s", "rock"])
(title: "Knockin\' on Heaven\'s Door", tags: @["70s", "rock"])
(title: "Emotion", tags: @["70s", "pop"])
(title: "The River", tags: @[])
Instead of just echo song
,
we can utilize writeLine(stdout, song)
.
Maybe Null
A not too simple, case example.
Consider make this data stucture more complex, with adding nullability option as shown in below type:
type
StringSeq = seq[string]
Song = tuple[title: string, tags: Option[StringSeq]]
The complete code can be shown as below:
import options
type
StringSeq = seq[string]
Song = tuple[title: string,
tags: Option[StringSeq]]
let songs: seq[Song] = @[
( title: "Cantaloupe Island",
tags : some(@["60s", "jazz"])),
( title: "Let It Be",
tags : some(@["60s", "rock"])),
( title: "Knockin' on Heaven's Door",
tags : some(@["70s", "rock"])),
( title: "Emotion",
tags : some(@["70s", "pop"])),
( title: "The River",
tags : none(StringSeq)),
]
for i, song in songs: song.echo
With the result similar as below record:
$ nim r t04_options.nim
(title: "Cantaloupe Island", tags: Some(@["60s", "jazz"]))
(title: "Let It Be", tags: Some(@["60s", "rock"]))
(title: "Knockin\' on Heaven\'s Door", tags: Some(@["70s", "rock"]))
(title: "Emotion", tags: Some(@["70s", "pop"]))
(title: "The River", tags: None[StringSeq])
This is just an example how maybe null
can be done in Nim
,
but we will discard this Option
example, for the rest of this article.
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:
type
StringSeq* = seq[string]
Song* = tuple[title: string, tags: StringSeq]
let songs*: seq[Song] = @[
( title: "Cantaloupe Island",
tags : @["60s", "jazz"]),
( title: "Let It Be",
tags : @["60s", "rock"]),
( title: "Knockin' on Heaven's Door",
tags : @["70s", "rock"]),
( title: "Emotion",
tags : @["70s", "pop"]),
( title: "The River",
tags : @[]),
]
Using Songs Module
Now we can have a very short code.
import MySongs
for i, song in songs: echo song
With the result exactly the same as above array.
3: Finishing The Task
MapIt, FilterIt, Flatten, Unique, Oneliner Chain
Extracting Fields
We can use map, or even better mapIt
.
And also chain the function with filterIt
,
in oneliner fashioned.
import sequtils
import MySongs
let tagsSeq = songs
.mapIt(it.tags)
.filterIt(it != @[])
for i, tags in tagsSeq: echo tags
With the result similar as below record:
$ nim compile --run --hints:off t06_filter
@["60s", "jazz"]
@["60s", "rock"]
@["70s", "rock"]
@["70s", "pop"]
The last tuple
with empty sequence
is not shown anymore.
Flatten
We can utilize foldl
from sequtils
library,
to flatten a nested sequence
.
The &
operator will concat sequence
for each iteration.
The a
is accumulator, and the b
is the value for each iteration.
import sequtils
import MySongs
let
tagsSeq: seq[StringSeq] = songs.mapIt(it.tags)
tags: StringSeq = tagsSeq.foldl(a & b)
echo tags
With the result similar as below sequence:
$ nim compile --run --hints:off t07_flatten
@["60s", "jazz", "60s", "rock", "70s", "rock", "70s", "pop"]
For convenience, I add the data type, so you can analyze further.
Unique
In order to get to get distinct
values,
we can convert a sequence
to either hash set
or ordered set
.
So I guess our problem is solved completely.
import sequtils, sets, MySongs
songs
.mapIt(it.tags)
.foldl(a & b)
.toOrderedSet
.toSeq
.echo
We can also chain every function,
and convert back from hash set
/ordered set
to sequence
,
before finally echo
the result,
with the result similar as below sequence
:
$ nim r t08_unique.nim
@["60s", "jazz", "rock", "70s", "pop"]
Now the code is very clear. We can understand exactly what it does, by just reading the code.
4: Alternate Approach
Since our introduction to nim
above is too short,
consider to makes it more complete with options
, proc
,
alternate flatten written in imperative fashioned.
Songs Module with Option
import options
type
StringSeq* = seq[string]
Song* = tuple[title: string,
tags: Option[StringSeq]]
let songs*: seq[Song] = @[
( title: "Cantaloupe Island",
tags : some(@["60s", "jazz"])),
( title: "Let It Be",
tags : some(@["60s", "rock"])),
( title: "Knockin' on Heaven's Door",
tags : some(@["70s", "rock"])),
( title: "Emotion",
tags : some(@["70s", "pop"])),
( title: "The River",
tags : none(StringSeq)),
]
Custom Flatten Function
Now we can have other approach for flattening the tags
This is simply flatten using all tag
s from all songs records,
using nested for in
loop.
import options
import MaSongs
proc flatten(songs: seq[Song]): seq[string] =
var tags: seq[string] = @[]
for i, song in songs:
if (song.tags != none(StringSeq)):
for ii, tag in song.tags.get():
tags.add(tag)
result = tags
echo songs.flatten()
With the result similar as below sequence
:
$ nim r t09_flatten.nim
@["60s", "jazz", "60s", "rock", "70s", "rock", "70s", "pop"]
We are going to utilize this imperative fashioned as a show case,
to pass data between thread
using channel
.
5: Concurrency with Channel
Concurrency in nim
, can be handled through channel
.
Based on flatten function above,
we are going to build show case to pass data between thread
.
Reference
The Skeleton
Now we should be ready for the real demo. This is only consist of one short file.
First we need a global channel
variable.
Then we have these three functions below:
import options
import MaSongs
var chan: Channel[Option[string]]
proc sender(songs: seq[Song]) = …
proc receiver() = …
proc runBoth() = …
runBoth()
- Sender
- Receiver
- runBoth (optionally)
Sender and Receiver
We should prepare two functions,
one for Sender
, and the other one for Receiver
.
The first one sending each results to Sender
channel,
as an option
with either some
value or none
.
proc sender(songs: seq[Song]) =
for i, song in songs:
if (song.tags != none(StringSeq)):
for ii, tag in song.tags.get():
chan.send(some(tag))
chan.send(none(string))
And from the Receiver
channel,
all the result would be gathered as sequence.
proc receiver() =
var tags: seq[string] = @[]
while true:
let tried = chan.tryRecv()
if tried.dataAvailable:
if tried.msg != none(string):
tags.add(tried.msg.get())
else:
echo tags
break
This way we can send the result through a channel,
instead of directly pushing the result to sequence
.
Running Both Thread
They do not aware of each other presence.
Consider gather both function in a function.
proc runBoth() =
# Initialize the channel.
chan.open()
# Launch the worker.
var worker1: Thread[seq[Song]]
createThread[seq[Song]](worker1, sender, songs)
var worker2: Thread[void]
createThread(worker2, receiver)
# Wait for the thread to exit
worker1.joinThread()
worker2.joinThread()
# Clean up the channel.
chan.close()
runBoth()
With the result similar as below sequence
:
$ nim r --threads:on t10_channel.nim
@["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 [ Crystal - Playing with Records ].