Preface
Goal: A practical case to collect unique record fields using Elixir.
After using erlang
for a while, it make sense to step into elixir
.
It turn out that elixir
is easy and also fun to work with.
It only take about eight hours to finish my task,
starting from blank knowledge.
This is compared with purescript
that took me for almost two days.
I’m so surprised, and asking to myself,
why don’t I learned this language years ago?
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
defmodule Song do
defstruct title: "", tags: []
end
defmodule Songs do
def songs() do
[
%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"}
]
end
end
Elixir Solution
The Answer
There might be many ways to do things in Elixir
.
One of them is this oneliner as below:
import Songs
IO.inspect songs()
|> Enum.map(& &1.tags)
|> :lists.append
|> Enum.uniq
The syntax, makes me cry. This is simply concise, an easy to understand.
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
No need any special setup. Just run and voila..!
1: Data Structure
Simple List, Simple Struct, and Songs
Simple List
Before building a struct
,
I begin with simple list.
tags = ["rock", "jazz", "rock", "pop", "pop"]
IO.inspect tags
With the result similar as below list:
$ elixir 01-list.exs
["rock", "jazz", "rock", "pop", "pop"]
It is easy to dump variable in elixir
using inspect
.
The Song Struct
We can continue our journey to records.
Everything in elixir
is module.
Even the struct
, defined in a module as shown below:
defmodule Song do
defstruct title: "", tags: []
end
defmodule Run do
song = %Song{
title: "Cantaloupe Island",
tags: ["60s", "jazz"]
}
IO.inspect song
end
With the result similar as below list:
$ elixir 02-record.exs
%Song{tags: ["60s", "jazz"], title: "Cantaloupe Island"}
List of Song Structs
Meet The Songs List
From just karaoke, we can go pro with recording studio.
From simple data, we can build a structure to solve our task.
defmodule Song do
defstruct title: "", tags: []
end
defmodule Run do
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.inspect songs
end
With the result similar as below inspect dump
:
$ elixir 03-songs.exs
[
%Song{tags: ["60s", "jazz"], title: "Cantaloupe Island"},
%Song{tags: ["60s", "rock"], title: "Let It Be"},
%Song{tags: ["70s", "rock"], title: "Knockin' on Heaven's Door"},
%Song{tags: ["70s", "pop"], title: "Emotion"},
%Song{tags: [], title: "The River"}
]
2: Separating Module
Since we need to reuse the songs record multiple times, it is a good idea to separate the data from logic.
Songs Module
The module file has .ex
extension,
different from elixir code with .exs
extension.
The code can be shown as below:
defmodule Song do
defstruct title: "", tags: []
end
defmodule Songs do
def songs() do
[
%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"}
]
end
end
In order to use the songs module, we need to compile the module first.
$ elixirc songs.ex
You should see two different generated files.
$ ls *.beam
Elixir.Song.beam Elixir.Songs.beam
- .
Using Songs Module
Now we can have a very short code.
defmodule Run do
import Songs
IO.inspect songs()
end
With the result exactly the same as previous code.
3: Finishing The Task
Map, Flatten and Unique
Extracting Field from Record
All at Once
Elixir has enough library so we can avoid making custom algorithm. We can just complete the task with this simple code:
defmodule Run do
import Songs
tags = for song <- songs(), do: song.tags
IO.inspect Enum.uniq(:lists.append(tags))
end
With the result similar as below list:
$ elixir 05-unique.exs
["60s", "jazz", "rock", "70s", "pop"]
We have this:
- Map using
for do
. :lists.Append
fromErlang
's standard library.Enum.uniq
fromElixir
's library.
Using Enum.Map
Elixir also comes with Enum.Map
.
This is very convenience for functional programmer
.
Now we can rewrite above code to below.
defmodule Run do
import Songs
tags = Enum.map(songs(), fn(song) -> song.tags end)
IO.inspect tags |> :lists.append |> Enum.uniq
end
It is also cooler to use pipe |>
to avoid too many parenthesis.
A kind guy from community named Adi Pur
hint me,
about pipe and capture operator.
This is a very useful information.
Using Capture Operator
We can rewrite the code above into this below
defmodule Run do
import Songs
tags = Enum.map(& &1.tags)
IO.inspect tags |> :lists.append |> Enum.uniq
end
Thank you to Adi Pur
.
Finally we have this cool oneliner fashioned as shown below:
import Songs
songs()
|> Enum.map(& &1.tags)
|> :lists.append
|> Enum.uniq
|> IO.inspect
4: Concurrency with Process
Instead of using coroutine
,
Elixir
is using Erlang Process
,
using Actor Model
pattern.
Reference
The Skeleton
We should be ready for the real demo. This is only consist of one short file.
defmodule MyProcess do
def walk, do …
def flatten(pid), do …
end
defmodule Run, do …
-
flatten()
- a
Sender
thatsend pid
to spawned pid,
- a
-
walk()
- a
Receiver
thatreceive
from spawned pid.
- a
-
Run()
- Program entry point
Sender
The flatten
function is an imperative style,
of pushing new tag to process.
def flatten(pid) do
import Songs
for song <- songs() do
for tag <- song.tags do
send pid, {:tag, tag}
end
end
end
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
.
def walk, do: walk([])
def walk(tags) do
receive do
{:tag, tag} ->
walk(tags++[tag])
{:quit} ->
tags |> Enum.uniq |> IO.inspect
end
end
Running Both Process
Pretty short right!
Consider gather both function in run
entry point.
defmodule Run do
pid = spawn(MyProcess, :walk, [])
MyProcess.flatten(pid)
send pid, {:quit}
end
With the result as below array
:
❯ elixir 08-process.exs
["60s", "jazz", "rock", "70s", "pop"]
This is the end of our Elixir
journey in this article.
We shall meet again in other article.
What is Next 🤔?
Consider continue reading [ Clojure - Playing with Records - Part One ].