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: Continue Part One


5: Approach in Solving Unique

A custom example, that you can rewrite.

An Algorithm Case

Why reinvent the wheel?

My intention is writing a custom pattern matching example, so later I can rewrite other algorithm based on this example. You can read the detail on the first article. The record overview.

Consider going back to simple data. We need to discuss about solving unique list.

x:xs Pattern Matching

The original x:xs pattern matching is made of two functions:

  1. Exclude
  2. Unique

Pattern matching in Julia required macro, and is not as fancy as haskell, or even OCaml. But what we have in Julia is enough to solve our little problem.

Exclude Function

The exclude function is just a filter with below details:

tags = ["rock", "jazz", "rock", "pop", "pop"]

function exclude(value)
  tags -> filter(tag -> tag!=value, tags)
end

tags |> exclude("rock") |> println

With the result as below array:

$ julia 21-exclude.jl
["jazz", "pop", "pop"]

Julia: Exclude Function

Recursive Unique Function

With exclude function above, we can build unique function recursively, as below code:

tags = ["rock", "jazz", "rock", "pop", "pop"]

exclude = function(value, tags)
  return [ tag for tag in tags if tag != value ]
end

unique = function(tags)
  if length(tags) <= 1
    return tags
  else
    head = popfirst!(tags)
    tail = unique(exclude(head, tags))
    return pushfirst!(tail, head)
  end  
end

tags |> unique |> println

With the result as below array:

$ julia 22-unique.jl
["rock", "jazz", "pop"]

Although the syntax looks is, not exactly the same as functional programming in haskell, the algorithm is the same.

Install Match Macro

Instead of just conditional, we can utilize third party macro named match. First we have to install using julia command line.

julia> using Pkg

julia> Pkg.add("Match")
 Installing known registries into `~/.julia`
######################################################################## 100.0%
      Added registry `General` to `~/.julia/registries/General`
  Resolving package versions...
  Installed Match ─ v1.1.0
Updating `~/.julia/environments/v1.5/Project.toml`
  [7eb4fadd] + Match v1.1.0
Updating `~/.julia/environments/v1.5/Manifest.toml`
  [7eb4fadd] + Match v1.1.0

Using Match Macro

Then we can use it as below code.

using Match

tags = ["rock", "jazz", "rock", "pop", "pop"]

exclude(value, tags) = begin
  filter(tag -> tag!=value, tags)
end

unique(tags)::Array{String} = begin
  @match tags begin
    []     => []
    [head] => [head]
    _      => begin
      head = popfirst!(tags)
      tail = unique(exclude(head, tags))
      pushfirst!(tail, head)
    end  
  end
end

tags |> unique |> println

With the result as below array:

$ julia 23-match.jl
["rock", "jazz", "pop"]

Julia: Using Match Macro

The reason that I put Array{String} string is, without the type signature we have this result below:

$ julia 23-match.jl
Any["rock", "jazz", "pop"]

One more thing, by the time of this article written, I still don’t know how to match with idiomatic [tail:head].

Generic Type

Both functions can should capable applied to any type, not just string. It is a good time to apply generic.

tags = ["rock", "jazz", "rock", "pop", "pop"]

function exclude(
  value::T, tags::Array{T}
)::Array{T} where {T<:Any}
  return [ tag for tag in tags if tag != value ]
end

function unique(
  tags::Array{T}
)::Array{T} where {T<:Any}
  if length(tags) <= 1
    return tags
  else
    head = popfirst!(tags)
    tail = unique(exclude(head, tags))
    return pushfirst!(tail, head)
  end  
end

tags |> unique |> println

With the result as below array:

$ julia 24-generics.jl
["rock", "jazz", "pop"]

Julia: Generics Type

Custom Utility Module

This would be great if we can reuse, the generic unique function above into its own module.

Instead of two functions, we can merge both into one, to reduce complexity.

module MyUtils

export unique

function unique(tags::Array{T})::Array{T} where {T<:Any}
  if length(tags) <= 1
    return tags
  else
    head = popfirst!(tags)
    excluded = [ tag for tag in tags if tag != head ]
    return pushfirst!(unique(excluded), head)
  end  
end

end

Julia: The Utility Module

Apply to Songs

The complete code is as below:

using Base.Iterators

include("MyUtils.jl")
include("MySongs.jl")
using .MySongs

[   song.tags for song in getSongs()
    if song.tags!=nothing
] |> flatten |> collect |> MyUtils.unique |> println

With the result as below array:

$ julia 25-songs.jl
["60s", "jazz", "rock", "70s", "pop"]

6: More About Function

Dive Deeper with Functional Programming

Function Composition

We can rewrite the previous code with function composition. This article below show a very nice trick, that you can use of in Julia

For practical reason, I flip the function order.

function ∘(f, g)
    x -> g(f(x))
end

Function composition is pretty common in functional world. Now we can compose the function as below:

[   song.tags for song in getSongs()
    if song.tags!=nothing
] |> (flatten ∘ collect ∘ unique ∘ println)

The complete code is as below:

using Base.Iterators

include("MySongs.jl")
using .MySongs

function ∘(f, g)
    x -> g(f(x))
end

[   song.tags for song in getSongs()
    if song.tags!=nothing
] |> (flatten ∘ collect ∘ unique ∘ println)

With the result as below array:

$ julia 14-composition.jl
["60s", "jazz", "rock", "70s", "pop"]

Julia: Function Composition


7: Concurrency with Channel

Julia support Actor Model pattern. We can rewrite those process above through channel.

Reference

The Skeleton

We should be ready for the real demo. This is only consist of one short file.

include("MyUtils.jl")
include("MySongs.jl")
using .MySongs

function flatten(messages::Channel) … end

function walk(messages::Channel) … end

function run() … end

run()
  1. Producer: flatten()
  2. Consumer: walk()
  3. Program entry point: run()

Producer and Consumer

We should prepare two functions:

  1. One for Producer that put! to channel,
  2. And the other one for Consumer that take! from channel.
function flatten(messages::Channel)
    songs::Array{Song} = getSongs()

    for song in songs
        if song.tags!=nothing
            for tag in song.tags
                put!(messages, tag)
            end
        end
    end
end

function walk(messages::Channel)
    newtags::Array{String} = []

    for tag in messages
        push!(newtags, tag)
    end
    
    return newtags
end

Julia: Concurreny with Channel: Producer and Consumer

Running Both Routine

Pretty short right!

Consider gather both function in run entry point.

function run()
    messages = Channel(flatten)
    walk(messages)     |>
        MyUtils.unique |>
        println
end

run()

With the result as below array:

$ julia 27-channel.jl
["60s", "jazz", "rock", "70s", "pop"]

Julia: Concurreny with Channel: Running Both Thread

This is the end of our Julia journey in this article. We shall meet again in other article.


What is Next 🤔?

Consider continue reading [ GNU R - Playing with Records ].