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

Julia: Simple Array of Tags

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

Julia: The Songs Module Containing List of Record

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: Using Songs Module

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()

Julia: Type Alias

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

Julia: Finishing The Task

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