Preface
Goal: A practical case to collect unique record fields using Erlang.
I decide to go further with Erlang
.
And I don’t even know why 😜.
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, tags = [] }).
songs() -> [
#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"}
].
Erlang Solution
The Answer
There might be many ways to do things in Erlang
.
One of them is this oneliner as below:
unique(List) -> sets:to_list(sets:from_list(List)).
show() ->
Songs = songs(),
Tags = [ Head#song.tags || Head <- Songs ],
io:fwrite("~60p~n", [unique(append(Tags))]).
Of course there are hidden details beneath each functions. I will discuss about this step by step.
Enough with introduction, at this point we should go straight to coding.
Environment
Just the erlang shell
.
1: Basic Data
Variable, Function and Constant
There three ways to represent data in erlang
:
- As variable,
- By function,
- As constant.
List
is the bread and butter of functional programming.
Before building a complex records,
I begin with simple datatype, the list
.
Data as Variable
This is the most commonly used.
The scope is inside the show()
function.
-module(t01_list).
-export([show/0]).
show() ->
Tags = ["rock", "jazz", "rock", "pop", "pop"],
io:fwrite("~60p~n", [Tags]).
With the result as below list (not array):
$ erl
Erlang/OTP 23 [erts-11.1.3] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [hipe]
Eshell V11.1.3 (abort with ^G)
1> c(01_list).
* 1: syntax error before: _list
1> c(t01_list).
{ok,t01_list}
2> t01_list:show().
["rock","jazz","rock","pop","pop"]
ok
3>
Data by Function
This is useful, when you need wider scope in module.
-module(t02_func).
-export([show/0]).
tags() -> ["rock", "jazz", "rock", "pop", "pop"].
show() ->
io:fwrite("~60p~n", [tags()]).
With the same result as above result, shown in below:
4> c(t02_func).
{ok,t02_func}
5> t02_func:show().
["rock","jazz","rock","pop","pop"]
Data as Constant
This is useful, when you need wider scope in module.
-module(t03_def).
-export([show/0]).
-define(Tags, ["rock", "jazz", "rock", "pop", "pop"]).
show() ->
io:fwrite("~60p~n", [?Tags]).
With the same result as above result.
No need to be shown again.
Custom Pretty Print
This io.fwrite
is powerful.
io:fwrite("~60p~n", [?Tags]).
The pretty print can be read fro official documentation:
2: Accessing Element
There at least two ways to access list element:
- Recursive Function
- List Comprehension
Recursive Function
-module(t04_element).
-export([show/0]).
print_each([]) -> ok;
print_each([Head|Tail]) ->
io:fwrite("~60p~n", [Head]),
print_each(Tail).
show() ->
Tags = ["rock", "jazz", "rock", "pop", "pop"],
print_each(Tags).
With the result similar as below string:
6> c(t04_element).
{ok,t04_element}
7> t04_element:show().
"rock"
"jazz"
"rock"
"pop"
"pop"
ok
List Comprehension
-module(t05_element).
-export([show/0]).
show() ->
Tags = ["rock", "jazz", "rock", "pop", "pop"],
[io:fwrite("~60p~n", [Head]) || Head <- Tags].
With the result similar as below string:
9> c(t05_element).
{ok,t05_element}
10> t05_element:show().
"rock"
"jazz"
"rock"
"pop"
"pop"
[ok,ok,ok,ok,ok]
3: Data Structure
We can continue our journey to records.
The Song Record
Writing record require -record
declaration.
-module(t06_rec).
-export([show/0]).
-record(song, { title, tags = [] }).
show() ->
Song = #song{
title = "Cantaloupe Island",
tags = ["60s", "jazz"]
},
io:fwrite("~60p~n", [Song#song.tags]).
With the result similar as below string:
11> c(t06_rec).
{ok,t06_rec}
12> t06_rec:show().
["60s","jazz"]
ok
Default Value
Instead of using nullability option
approach like OCaml
,
or Maybe
monad approach just like Haskell
,
Erlang
simply use default
value.
-record(song, { title, tags = [] }).
List of Song Records
-module(t07_songs).
-export([show/0]).
-record(song, { title, tags = [] }).
show() ->
Songs = [
#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"}
],
[ io:fwrite("~60p~n", [Head#song.tags])
|| Head <- Songs
].
With the result similar as below string:
13> c(t07_songs).
{ok,t07_songs}
14> t07_songs:show().
["60s","jazz"]
["60s","rock"]
["70s","rock"]
["70s","pop"]
[]
[ok,ok,ok,ok,ok]
4: Exporting Record
Since we need to reuse the songs record multiple times, it is a good idea to separate the data from logic. In order to use this we require two files:
- Header File.
- Module File.
And of course the main file contain the logic.
Header File
The header file can be shown as below:
-record(song, { title, tags = [] }).
Module File
The module file can be shown as below:
-module(my_songs).
-export([songs/0]).
-include("my_header.hrl").
songs() -> [
#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"}
].
Do not forget to include the header file.
-include("my_header.hrl").
In order to use the songs module, we need to compile the module first.
17> c(my_songs).
{ok,my_songs}
Importing Records from Module
The logic file can be shown as below. Also do not forget to include the header file.
-module(t08_import).
-import(my_songs, [songs/0]).
-export([show/0]).
-include("my_header.hrl").
show() ->
Songs = songs(),
Tags = [ Head#song.tags || Head <- Songs ],
io:fwrite("~60p~n", [Tags]).
With the result similar as below:
17> c(my_songs).
{ok,my_songs}
18> c(t08_import).
{ok,t08_import}
19> t08_import:show().
[["60s","jazz"],
["60s","rock"],
["70s","rock"],
["70s","pop"],
[]]
ok
Now we have a shorter code.
5: Finishing The Task
Flatten and Unique
As usual we need this two subtask:
- Flatten.
- Unique (Distinct).
Flatten
We can simply apply standard append
function to flatten the list.
-module(t09_flatten).
-import(my_songs, [songs/0]).
-import(lists, [append/1]).
-export([show/0]).
-include("my_header.hrl").
show() ->
Songs = songs(),
Tags = [ Head#song.tags || Head <- Songs ],
io:fwrite("~60p~n", [append(Tags)]).
With the result similar as below:
20> c(t09_flatten).
{ok,t09_flatten}
21> t09_flatten:show().
["60s","jazz","60s","rock","70s","rock","70s","pop"]
ok
Unique (Distinct)
And for unique, we just need to pass the list
into sets
,
and convert back into a list
. All with standard function.
-module(t10_unique).
-import(my_songs, [songs/0]).
-import(lists, [append/1]).
-import(set, [from_list/1, to_list/1]).
-export([show/0]).
-include("my_header.hrl").
unique(List) -> sets:to_list(sets:from_list(List)).
show() ->
Songs = songs(),
Tags = [ Head#song.tags || Head <- Songs ],
io:fwrite("~60p~n", [unique(append(Tags))]).
With the result similar as below:
22> c(t10_unique).
{ok,t10_unique}
23> t10_unique:show().
["jazz","60s","70s","rock","pop"]
ok
6: Concurrency with Process
Erlang
can handle concurrency,
with Sender
and Receiver
model.
Reference
Custom Flatten: Nested Function
In order to make a Sender
demo,
I need to make custom handmade flatten function.
flatten_inner([]) -> ok;
flatten_inner([Head|Tail]) ->
io:fwrite("~p ", [Head]),
flatten_inner(Tail).
flatten([]) -> ok;
flatten([Head|Tail]) ->
flatten_inner(Head#song.tags),
flatten(Tail).
show() ->
Songs = songs(),
flatten(Songs),
io:fwrite("\n").
With the result similar as below vector
:
69> c(t11_iterate).
{ok,t11_iterate}
70> t11_iterate:show().
"60s" "jazz" "60s" "rock" "70s" "rock" "70s" "pop"
ok
Custom Flatten: Nested For Loop
However, we can rewrite the nested function above in imperative style. This nested loop below is considered simpler to be understood.
flatten(Songs) ->
foreach(fun(S) ->
foreach(fun(T) ->
io:fwrite("~p ", [T])
end, S#song.tags)
end, Songs).
This is simply flatten using all tag
s from all songs records,
using nested for loop
.
This way we can send the result to a Sender
channel,
instead of directly pushing the result to vector
.
The Skeleton
We should be ready for the real demo. This is only consist of one short file.
walk() -> …
flatten(PID) -> …
show() -> …
-
flatten()
- a
Sender
thatsend
to spawned pid,
- a
-
walk()
- a
Receiver
thatreceive
from spawned pid.
- a
-
show()
- Program entry point
Do not forget to give sufficient top declaration.
-module(t13_process).
-import(my_songs, [songs/0]).
-import(lists, [foreach/2]).
-import(lists, [append/2]).
-export([show/0, walk/0, flatten/1]).
-include("my_header.hrl").
Sender
The flatten
function is pushing new tag to process.
flatten(PID) ->
Songs = songs(),
foreach(fun(S) ->
foreach(fun(T) ->
PID ! {"tags", T}
end, S#song.tags)
end, Songs),
PID ! quit.
We would gather all process later to form a new tags list.
Receiver
The walk
is a recursive function,
consist of two forms with different arity:
walk/0
, andwalk/1
.
walk() -> walk([]).
walk(Tags) ->
receive
{"tags", T} ->
walk([T] ++ Tags);
quit ->
io:fwrite("~60p~n", [Tags])
end.
Running Both Process
Pretty short right!
Consider gather both function in show()
entry point.
show() ->
PID = spawn(t13_process, walk, []),
spawn(t13_process, flatten, [PID]),
io:fwrite("").
With the result as below array
:
66> c(t13_process).
{ok,t13_process}
67> t13_process:show().
["pop","70s","rock","70s","rock","60s","jazz","60s"]
ok
This is the end of our Erlang
journey in this article.
We shall meet again in other article.
What is Next 🤔?
Consider continue reading [ Elixir - Playing with Records ].