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 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:

  1. As variable,
  2. By function,
  3. 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> 

Erlang: Simple List of Tags

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:

  1. Recursive Function
  2. 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]

Erlang: Accessing Element


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]

Erlang: Accessing Element


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:

  1. Header File.
  2. 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").

Erlang: The Song Header and Module

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

Erlang: Importing Records from Module

Now we have a shorter code.


5: Finishing The Task

Flatten and Unique

As usual we need this two subtask:

  1. Flatten.
  2. 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

Erlang: Flatten and Unique (Distinct)


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 tags 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() -> …
  1. flatten()

    • a Sender that send to spawned pid,
  2. walk()

    • a Receiver that receive from spawned pid.
  3. 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, and
  • walk/1.
walk() -> walk([]).

walk(Tags) ->
  receive
    {"tags", T} ->
      walk([T] ++ Tags);
    quit ->
      io:fwrite("~60p~n", [Tags])
  end.

Erlang: Concurreny with Process: Sender and Receiver

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

Erlang: Concurreny with Process: Sender and Receiver

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