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!
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!
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 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}
}
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]
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" })
])
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
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)
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.
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 ].