Preface
Goal: Continue Part One.
I would like to continue with concurrency,
but the thing is the previously example code,
is not enough to cover most part of clojure
.
We cannot just jump to this intermediate topic,
with lack of basic code introduction,
So I dedicate this article to cover basic clojure
trick.
5: Alternate Approach
Since our introduction to clojure
above is too short,
consider to makes it more complete
alternate flatten written in imperative fashioned.
Then we are going to continue with recursive example,
and also, with head and tail x:xs
pattern,
until finally conclude with our songs task with alternate solution.
Clojure Project File
We need to a more setup, so that all script in this artcile could be handled.
:profiles {…
:t10-flatten {:main t10-flatten}
:t11-tags-exclude {:main t11-tags-exclude}
:t12-tags-unique {:main t12-tags-unique}
:t13-recursion {:main t13-recursion}
:t13-songs-unique {:main t13-songs-unique}
}
Custom Flatten Using doseq
Not every real life code is functional.
Some case such as concurrency are naturally comes from side effect.
Thus we need to prepare to handle this,
without map
, filter
, or reduce
.
We can write our custom flatten using doseq
.
This doseq
is very similar with for loop.
(ns t10-flatten (:gen-class) (:use my-songs))
(defn -main [& args]
(doseq [song songs]
(if (not (nil? (:tags song)))
(doseq [tag (:tags song)]
(println tag)
))))
With the result similar as below string
:
$ lein with-profile t10-flatten run
60s
jazz
60s
rock
70s
rock
70s
pop
Imperative Flatten Using Atom
Or better, we can save the value using atom
.
This flatten
function is just an imperative style,
of pushing new tag to array.
(ns t10-flatten (:gen-class) (:use my-songs))
(defn get-tags [songs]
(def tags (atom []))
(doseq [song songs]
(if (not (nil? (:tags song)))
(doseq [tag (:tags song)]
(swap! tags conj tag))))
(deref tags))
(defn -main [& args]
(println (get-tags songs)))
With the result similar as below vector:
$ lein with-profile t10-flatten run
[60s jazz 60s rock 70s rock 70s pop]
We would use this function as an example concurrency code later.
Simple Recursion Example
Atom is not the only solution, we can write this in functional fashioned using recursive. Consider this simple recursion example before we apply in concurency.
(ns t13-recursion (:gen-class))
(defn collect-random
([]
(collect-random []))
([numbers]
(if (<= (count numbers) 10)
(collect-random (conj numbers (rand-int 100)))
(println numbers))
))
(defn -main [& args] (collect-random))
With the result similar as below vector
:
$ lein with-profile t13-recursion run
[91 72 26 58 95 94 49 16 97 11 93]
As you see in above recursion, we have multi-arity pattern. This is a very common pattern.
Head and Tail Pattern Matching
Why making things complex?
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.
The original x:xs
pattern matching from haskell
shown as below:
exclude :: String -> ([String] -> [String])
exclude value = filter((/=) value)
unique :: [String] -> [String]
unique [] = []
unique (head:tail) = head:unique(exclude head tail)
tags :: [String]
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = print (unique tags)
With the result of
["rock","jazz","pop"]
This x:xs
pattern is made of two functions:
- Exclude
- Unique
Loop for Exclude
Based on
exclude :: String -> ([String] -> [String])
exclude value = filter((/=) value
We can rewrite twoliners above to this below:
(ns t11-tags-exclude (:gen-class))
(def tags ["rock" "jazz" "rock" "pop" "pop"])
(defn exclude [value items]
(filter (fn [item] (not= value item)) items)
)
(defn -main [& args] (
println (exclude "rock" tags)
))
Or even shorter with
(defn exclude [value items]
(filter #(not= value %) items))
With the result similar as below vector
:
$ lein with-profile t11-tags-exclude run
(jazz pop pop)
Loop for Unique
Based on
unique :: [String] -> [String]
unique [] = []
unique (head:tail) = head:unique(exclude head tail)
We can rewrite threeliners above with recursive function as below code:
(ns t12-tags-unique (:gen-class))
(def tags ["rock" "jazz" "rock" "pop" "pop"])
(defn exclude [value items]
(filter #(not= value %) items))
(defn unique [items]
(if (<= (count items) 1)
items
(let [[head & tail] items]
(concat [head] (unique
(exclude head tail))))
))
(defn -main [& args] (
println (unique tags)
))
With the result similar as below vector
:
$ lein with-profile t12-tags-unique run
(rock jazz pop)
The Recursive Head and Tail
Based on
… = head:unique(exclude head tail)
Transformed into three possibilities:
-
[]
, return[]
(empty vector). -
[head]
, return[head]
(vector with single value). We can also wrapped both[]
and[head]
, by just returningitems
. -
[head:tail]
, returning recursive function[head, new-tail]
.
(let [[head & tail] items]
(concat [head] (unique
(exclude head tail))))
I guess the code itself is self explanatory.
Put All The String Together
We can apply all to finish our previous songs task, with entirely different approach.
(ns t13-songs-unique (:gen-class) (:use my-songs))
(defn get-tags [songs]
(def tags (atom []))
(doseq [song songs]
(if (not (nil? (:tags song)))
(doseq [tag (:tags song)]
(swap! tags conj tag))))
(deref tags))
(defn exclude [value items]
(filter #(not= value %) items))
(defn unique [items]
(if (<= (count items) 1)
items
(let [[head & tail] items]
(concat [head] (unique
(exclude head tail))))
))
(defn -main [& args]
(->> songs
(get-tags)
(unique)
(println)
))
With the result similar as below vector
:
$ lein with-profile t13-songs-unique run
(60s jazz rock 70s pop)
What is Next 🤔?
We should be ready for our next topic, concurrency case example.
Consider continue reading [ Clojure - Playing with Records - Concurrency ].