Preface
Goal: A practical case to collect unique record fields using Julia.
This is just great. I think I’m in love with this Julia
language.
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
struct Song
title::String
tags::Union{Array{String}, Nothing}
end
function getSongs()::Array{Song}
return [
Song("Cantaloupe Island",
["60s", "jazz"]),
Song("Let It Be",
["60s", "rock"]),
Song("Knockin' on Heaven's Door",
["70s", "rock"]),
Song("Emotion",
["70s", "pop"]),
Song("The River", nothing)
]
end
Julia Solution
The Answer
There might be many ways to do things in Julia
.
One of them is this oneliner as below:
[ song.tags for song in getSongs()
if song.tags!=nothing
] |> flatten |> collect |> unique |> println
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 = ["rock", "jazz", "rock", "pop", "pop"]
println(tags)
It is easy to dump variable in julia
using println
.
With the result similar as below array
:
$ julia 01-tags.jl
["rock", "jazz", "rock", "pop", "pop"]
Avoid Global Variable
Global variable is discouraged in Julia. We should change our workflow, such as put the variable inside a function. You can use any function name.
"Use function to avoid global variable."
function main()
tags::Array{String} =
["rock", "jazz", "rock", "pop", "pop"]
println(tags)
return nothing
end
main()
With the result exactly the same as above array.
The Song Struct
We can continue our journey to records using struct
.
Here we use Song
constructor, defined by struct.
struct Song
title::String
tags::Array{String}
end
"You can use any function name."
function run()::Nothing
song = Song("Cantaloupe Island",
["60s", "jazz"])
println(song)
return nothing
end
run()
With the result similar as below record:
$ julia 03-struct.jl
Song("Cantaloupe Island", ["60s", "jazz"])
To define void function.
we can add ::Nothing
type as output signature.
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.
struct Song
title::String
tags::Array{String}
end
"Print each using map."
function run()::Nothing
songs::Array{Song} = [
Song("Cantaloupe Island",
["60s", "jazz"]),
Song("Let It Be",
["60s", "rock"]),
Song("Knockin' on Heaven's Door",
["70s", "rock"]),
Song("Emotion",
["70s", "pop"]),
Song("The River", [])
]
map(println, songs)
return nothing
end
run()
With the result similar as below sequential lines of Song
type:
$ julia 04-songs.jl
Song("Cantaloupe Island", ["60s", "jazz"])
Song("Let It Be", ["60s", "rock"])
Song("Knockin' on Heaven's Door", ["70s", "rock"])
Song("Emotion", ["70s", "pop"])
Song("The River", String[])
Instead of just println
,
I iterate the array
using songs map
.
Maybe Null
We need, a not too simple, case example.
Consider make this data stucture more complex, with adding nullability option as shown in below struct:
struct Song
title::String
tags::Union{Array{String}, Nothing}
end
Julia
using a cool design pattern with Union{SomeType, Nothing}
,
to solve nullability option, so tags
can accept nothing
value,
instead of just empty array.
struct Song
title::String
tags::Union{Array{String}, Nothing}
end
function run()
songs::Array{Song} = [
Song("Cantaloupe Island",
["60s", "jazz"]),
Song("Let It Be",
["60s", "rock"]),
Song("Knockin' on Heaven's Door",
["70s", "rock"]),
Song("Emotion",
["70s", "pop"]),
Song("The River", nothing)
]
map(song -> println(song.tags), songs)
end
run()
With the result similar as below record:
$ julia 05-union.jl
["60s", "jazz"]
["60s", "rock"]
["70s", "rock"]
["70s", "pop"]
nothing
You can spot the ide on how julia
handle null value,
for the last record.
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:
module MySongs
export Song, getSongs
struct Song
title::String
tags::Union{Array{String}, Nothing}
end
function getSongs()::Array{Song}
return [
Song("Cantaloupe Island",
["60s", "jazz"]),
Song("Let It Be",
["60s", "rock"]),
Song("Knockin' on Heaven's Door",
["70s", "rock"]),
Song("Emotion",
["70s", "pop"]),
Song("The River", nothing)
]
end
end
Using Songs Module
Now we can have a very short code.
include("MySongs.jl")
map(MySongs.getSongs()) do song
println(song.tags)
end
With the result exactly the same as above array.
Julia
has this map do
notation.
3: Extracting Fields
Map, Filter, Type Alias, List Comprehension
Filter
We have seen map
in previous code,
we are going to continue with filter.
We can do it all with oneliner,
just like commonly coding in functional programming.
include("MySongs.jl")
using .MySongs
"Shorter namespace."
function run()
songs = getSongs()
maybe_tags = map(song -> song.tags, songs)
all_tags = filter(tag -> tag!=nothing, maybe_tags)
map(println, all_tags)
end
run()
With the result similar as below record:
$ julia 07-filter.jl
["60s", "jazz"]
["60s", "rock"]
["70s", "rock"]
["70s", "pop"]
The last record with nothing value is shown nomore.
Type Alias
We can push strict static types to examine further behaviour.
include("MySongs.jl")
using .MySongs
const MaybeTags = Array{Union{Array{String}, Nothing}}
const AllTags = Array{Array{String}}
"You can remove all types for a more clean logic."
function run()
songs::Array{Song} = getSongs()
maybe_tags::MaybeTags =
map(song -> song.tags, songs)
all_tags::AllTags =
filter(tag -> tag!=nothing, maybe_tags)
map(println, all_tags)
end
run()
With the result exactly the same as above sequential lines of array.
List Comprehension
Instead of doing the iteration twice, we can utilize list comprehension.
include("MySongs.jl")
using .MySongs
"Using list comprehension."
function run()
songs::Array{Song} = getSongs()
all_tags::Array{Array{String}} = [
song.tags for song in songs
if song.tags!=nothing
]
map(println, all_tags)
end
run()
With the result exactly the same as above sequential lines of array.
4: Finishing The Task
Flatten, Unique, Oneliner Pipe
Flatten
There is already a flatten
function in standard library,
using Base.Iterators
, but it return iterators
instead of array
.
To get a proper result, we can utilize collect
function.
using Base.Iterators
include("MySongs.jl")
using .MySongs
"Using collect() to unwrap iterator."
function run()
songs::Array{Song} = getSongs()
all_tags::Array{Array{String}} = [
song.tags for song in songs
if song.tags!=nothing
]
println(collect(flatten(all_tags)))
end
run()
With the result similar as below array:
$ julia 10-flatten.jl
["60s", "jazz", "60s", "rock", "70s", "rock", "70s", "pop"]
Unique
There is also a unique
function in standard library.
So I guess our problem is solved completely.
using Base.Iterators
include("MySongs.jl")
using .MySongs
"Using standard libraries."
function run()
songs::Array{Song} = getSongs()
all_tags::Array{Array{String}} = [
song.tags for song in songs
if song.tags!=nothing
]
flatten_tags::Array{String} =
collect(flatten(all_tags))
println(unique(flatten_tags))
end
run()
With the result similar as below array:
$ julia 11-unique.jl
["60s", "jazz", "rock", "70s", "pop"]
Oneliner
Without global variable, We can completely remove function, and make this to be a single oneliner statement.
using Base.Iterators
include("MySongs.jl")
using .MySongs
println(unique(collect(flatten([
song.tags for song in getSongs()
if song.tags!=nothing
]))))
This is simple, but hard to be read.
Pipe
Luckily, we have this pipe operator, as syntatic sugar to above form.
using Base.Iterators
include("MySongs.jl")
using .MySongs
[ song.tags for song in getSongs()
if song.tags!=nothing
] |> flatten |> collect |> unique |> println
Now the code is very clear. We can understand exactly what it does, by just reading the code.
What is Next 🤔?
Consider continue reading [ Julia - Playing with Records - Part Two ].