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

Clojure is great, it has LISP syntax, easy to learn with. But the environment setup, would confuse beginner.

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

(ns my-songs (:gen-class))

(defrecord Song [title tags])
(def songs [
    (map->Song {
      :title "Cantaloupe Island"
      :tags ["60s" "jazz"] })
    (map->Song {
      :title "Let It Be"
      :tags ["60s" "rock"] })
    (map->Song {
      :title "Knockin' on Heaven's Door"
      :tags ["70s" "rock"] })
    (map->Song {
      :title "Emotion"
      :tags ["70s" "pop"] })
    (map->Song {
      :title "The River" })
  ])

Clojure Solution

The Answer

There might be many ways to do things in Clojure. One of them is this oneliner as below:

(ns t08-unique (:gen-class) (:use my-songs))

(defn extract
  [songs]
  (remove nil? (map #(get % :tags) songs))
)

(defn -main [& args]
    (println (distinct (flatten
       (extract songs)
    )))
  )

I’m so happy that I finally use lisp syntax.

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

I spent a few first hours figuring out, how to setup multiple entry point with clojure. Until I finally get it right with Leiningen build automation tool.

Text Editor

Since this clojure takes a different directories, and I do not want to switch between tiling manager, just to use file manager, I’d rather use xdg-open. This is rather out of topic. But I put my setting here anyway.

$ xdg-mime query filetype aloha.clj
text/x-clojure
$ xdg-mime default geany.desktop text/x-clojure
$ xdg-open aloha.clj

1: Leiningen

Setup for Use with Multiple Script.

Leiningen is build automation tool for clojure. Configuration done in project.cli. We should edit project.cli often.

JVM Options

When running Lein, I face this message.

$ lein run main
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.

I can surpress the warning, by exporting enviroment as below:

$ export LEIN_JVM_OPTS="-XX:TieredStopAtLevel=1"

Running Single Script

As always, I started with simple script.

(println "Aloha world!")

But how do I run this? This is my first attempt.

$ clojure -M scripts/aloha.clj
Aloha world!

Clojure: Execute Script with Clojure

Lein Exec

This means I must configure project.cli, and add lein-exec plugin.

  :plugins [[lein-exec "0.3.7"]]

Now I can run with:

$ lein exec scripts/aloha.clj
Aloha world!

Aliases

We can some kind of shortcut to works with Lein.

  :aliases {"alohascript" ["exec" "scripts/aloha.clj"]}

Now we can call the script in a shorter fashioned.

$ lein alohascript
Aloha world!

Clojure: Execute Script with Leiningen

Profiles

Multiple Entry Point

I can run two or more script with the same main entry point, by setting in project.clj as below:

  :profiles {
             :hola  {:main hola}
             :aloha {:main aloha}
          }
  :aliases {"alohascript" ["exec" "scripts/aloha.clj"]
            "hola"  ["with-profile" "hola"  "run"]
            "aloha" ["with-profile" "aloha" "run"]
          }

Wih each script as below:

(ns aloha (:gen-class))
(defn -main [& args] (println "Aloha world!"))
(ns hola (:gen-class))
(defn -main [& args] (println "Hola world!"))

Combined with alias, we can run as below:

$ lein hola
Hola world!
$ lein aloha
Aloha world!

Clojure: Multiple Entry Point with Leiningen

Clojure Project File

Since I need to explain step by step, I setup the project.clj, with all scripts included.

  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}
             :hola  {:main hola}
             :aloha {:main aloha}
             :t01-tags     {:main t01-tags}
             :t02-record   {:main t02-record}
             :t03-songs    {:main t03-songs}
             :t04-module   {:main t04-module}
             :t05-extract  {:main t05-extract}
             :t06-function {:main t06-function}
             :t07-flatten  {:main t07-flatten}
             :t08-unique   {:main t08-unique}
             :t09-pipe     {:main t09-pipe}
          }

Clojure: All Project Scripts


2: Data Structure

It is common to use vector (array) in clojure.

Simple Array

Before building a records, I begin with simple vector.

(ns t01-tags (:gen-class))
(def tags ["rock" "jazz" "rock" "pop" "pop"])

(defn -main [& args] (println tags))

It is easy to dump variable in clojure using println. With the result similar as below vector:

$ lein with-profile t01-tags run
[rock jazz rock pop pop]

Clojure: Simple Vector of Tags

You might notice the difference between the namespace t01-tags. And the underscore in filename src/t01_tags.clj. And also in the project.cli.

  :profiles {:t01-tags {:main t01-tags}}

The Song Record

We can continue our journey to records, as shown below:

(ns t02-record (:gen-class))

(defrecord Song [title tags])
(def song (map->Song {
      :title "Cantaloupe Island" 
      :tags ["60s" "jazz"]
    })
  )

(defn -main [& args] (println song))

With the result similar as below record:

$ lein with-profile t02-record run
#t02_record.Song{:title Cantaloupe Island, :tags [60s jazz]}

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

(ns t03-songs (:gen-class))

(defrecord Song [title tags])
(def songs [
    (map->Song {
      :title "Cantaloupe Island"
      :tags ["60s" "jazz"] })
    (map->Song {
      :title "Let It Be"
      :tags ["60s" "rock"] })
    (map->Song {
      :title "Knockin' on Heaven's Door"
      :tags ["70s" "rock"] })
    (map->Song {
      :title "Emotion"
      :tags ["70s" "pop"] })
    (map->Song {
      :title "The River" })
  ])

(defn -main [& args]
    (doseq [song songs] (println song))
  )

With the result similar as below sequential lines of records:

$ lein with-profile t03-songs run
#t03_songs.Song{:title Cantaloupe Island, :tags [60s jazz]}
#t03_songs.Song{:title Let It Be, :tags [60s rock]}
#t03_songs.Song{:title Knockin' on Heaven's Door, :tags [70s rock]}
#t03_songs.Song{:title Emotion, :tags [70s pop]}
#t03_songs.Song{:title The River, :tags nil}

Instead of just println, I iterate the vector using doseq.


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

(ns my-songs (:gen-class))

(defrecord Song [title tags])
(def songs [
    (map->Song {
      :title "Cantaloupe Island"
      :tags ["60s" "jazz"] })
    (map->Song {
      :title "Let It Be"
      :tags ["60s" "rock"] })
    (map->Song {
      :title "Knockin' on Heaven's Door"
      :tags ["70s" "rock"] })
    (map->Song {
      :title "Emotion"
      :tags ["70s" "pop"] })
    (map->Song {
      :title "The River" })
  ])

Clojure: The Songs Module Containing Vector of Record

You can spot the ide on how clojure handle null value, for the last record.

Using Songs Module

Now we can have a very short code.

(ns t04-module (:gen-class) (:use my-songs))

(defn -main [& args]
    (doseq [song songs] (println song))
  )

With the result exactly the same as previous code.


4: Finishing The Task

Map, Filter, Flatten and Unique

Extracting Field Using Map

All at Once

Just like anty other functiona programming, we can have a lot of fun with map.

(ns t05-extract (:gen-class) (:use my-songs))

(def extract (map #(get % :tags) songs))

(defn -main [& args]
    (doseq [tags extract] (println tags))
  )

With the result similar as below vector:

$ lein with-profile t05-extract run
[60s jazz]
[60s rock]
[70s rock]
[70s pop]
nil

Clojure: Extracting Field Using Map

The last nil exist, because we do not have any tags for the last song.

Regular Function

Instead of using def to create module scope variable, we can utilize defn with simple songs parameter argument.

(ns t06-function (:gen-class) (:use my-songs))

(defn extract
  [songs]
  (remove nil? (map #(get % :tags) songs))
)

(defn -main [& args]
    (doseq [tags (extract songs)]
      (println tags))
  )

With the result similar as below vector:

$ lein with-profile t06-function run
[60s jazz]
[60s rock]
[70s rock]
[70s pop]

I also put filter to remove the nil value.

Flatten

Since we have vector of vector, we need to flatten this nested vector into just a simple vector. We can simply apply standard flatten to the vector.

(ns t07-flatten (:gen-class) (:use my-songs))

(defn extract
  [songs]
  (remove nil? (map #(get % :tags) songs))
)

(defn -main [& args]
    (println (flatten (extract songs)))
  )

With the result similar as below vector:

$ lein with-profile t07-flatten run
(60s jazz 60s rock 70s rock 70s pop)

Unique (Distinct)

Clojure has enough library so we can avoid making custom algorithm. We can just complete the task with applying, standard distinct to the vector.

(ns t08-unique (:gen-class) (:use my-songs))

(defn extract
  [songs]
  (remove nil? (map #(get % :tags) songs))
)

(defn -main [& args]
    (println (distinct (flatten
       (extract songs)
    )))
  )

With the result similar as below:

$ lein with-profile t08-unique run
(60s jazz rock 70s pop)

Clojure: Flatten and Unique (Distinct)

Pipe using Thread Last Operator

We need a cool final code right 😄? Consider using thread-last operator (->>) syntatic sugar, to make the onliner statement easier to be read.

(ns t09-pipe (:gen-class) (:use my-songs))

(defn extract
  [songs]
  (remove nil? (map #(get % :tags) songs))
)

(defn -main [& args]
  (->> songs
       (extract) 
       (flatten)
       (distinct)
       (println)))

With the result exacly as previous result.:

$ lein with-profile t09-pipe run
(60s jazz rock 70s pop)

We can easily read, the step of each process, with this thread-last (->>) operator.

Clojure: Pipe using Thread Last Operator

We can even write as this below:

(defn -main [& args]
  (->> songs extract flatten distinct println))

Finally finished, without any significant obstacles.


What is Next 🤔?

Since our introduction above is too short we need more case example. We can rewrite those solution above with alternative approach. More example code, means more knowledge.

Consider continue reading [ Clojure - Playing with Records - Part Two ].