Learning Clojure Part 5: Robot Factory

Pavel Polívka - Jan 11 '21 - - Dev Community

In the previous 4 parts, we learned a lot, it's time to do something little big bigger and learn something new in the process.
Let's imagine we have a little robot assembly factory. We are ordering all the parts from China. We figured out that parts that come in pairs (eyes, hands, legs, etc..) are easier and cheaper to order with only the left variant and we make the matching right one by our self.

The code to make those right parts is our first goal here.

We receive parts like this:

(def robot-parts [
 {:name "head" :price 50}
 {:name "left-eye" :price 15}
 {:name "neck" :price 10}
 {:name "left-hand" :price 20}
 {:name "torso" :price 50}
 {:name "left-leg" :price 20}
])
Enter fullscreen mode Exit fullscreen mode

It's a vector of maps, each map has the name of the part and it's target price. Later we will sum the prices to figure out our costs.

Complete the robot

The first thing we need to be able to do is to create a right part from the left one.
Let's create this simple function:

(defn make-right-part
 [left-part]
 {
 :name (clojure.string/replace (:name left-part) #"^left-" "right-")
 :price (:price left-part)
 }
)
Enter fullscreen mode Exit fullscreen mode

regex

Notice we used a regular expression here. Clojure regular expressions work pretty much similar to regexes in other languages. The notation for a regex is

#"regular-expression"
Enter fullscreen mode Exit fullscreen mode

We can play with it a bit using the re-find function.

(re-find #"^left-" "left-eye")
;=> "left-"

(re-find #"^left-" "head")
;=> nil
Enter fullscreen mode Exit fullscreen mode

Using our expression, our function will output "right-eye" for a "left-eye" input and other strings, that are not starting with left will return untouched.

(make-right-part {:name "left-hand" :price 1})
;=> {:name "right-hand", :price 1}

(make-right-part {:name "right-hand" :price 1})
;=> {:name "right-hand", :price 1}
Enter fullscreen mode Exit fullscreen mode

OK. Now we can create the right part from the left one. We can create a function that will complete the robot.

(defn complete-robot
 [parts]
 (loop [remaining parts final []]
 (if (empty? remaining)
 final
 (let [[part & rest] remaining]
 (recur rest (into final (set [part (make-right-part part)]))) 
 )
 )

 )
)
Enter fullscreen mode Exit fullscreen mode

Uggh. There is a lot of new stuff here. Let's go over it.

loop

As the main body of our complete function, we are using the loop function. Loop behaves differently than loops in other languages. Loop is another way how to write recursion.

Lets do a simple loop example:

(loop [i 0]
 (println (str "i=" i))
 (if (> i 2)
 (println "END")
 (recur (inc i))
 )
)
;i=0
;i=1
;i=2
;i=3
;END
Enter fullscreen mode Exit fullscreen mode

Let's break it down. The first line begins the loop and introduces an i variable with the initial value. We can introduce more variables here.
The second line is the body of a loop. That is the action that will be done each iteration. The third line is a condition that will break the loop when we should end it. In the else part of the condition, we are calling the recur function, that is telling the loop to do the next round (continue the recursion), we are providing the new attribute values here.

let

Another new piece into the puzzle is let. It establishes the new scope and binds values to names. We can use the same tricks for arguments as we can do in functions.

(let [i 42] i)
;=> 42

(def characters ["Darth Vader" "Yoda" "Obi-Wan"])
(let [[main & rest] characters]
 (str "Main character is " main " rest are " rest)
)
;=> "Main character is Darth Vader rest are (\"Yoda\" \"Obi-Wan\")"
Enter fullscreen mode Exit fullscreen mode

reduce

Right now we have all the parts explained and our function for completing robots works.

(complete-robot robot-parts)
;[{:name "head", :price 50}
;{:name "right-eye", :price 15}
;{:name "left-eye", :price 15}
;{:name "neck", :price 10}
;{:name "left-hand", :price 20}
;{:name "right-hand", :price 20}
;{:name "torso", :price 50}
;{:name "right-leg", :price 20}
;{:name "left-leg", :price 20}]
Enter fullscreen mode Exit fullscreen mode

The code we wrote (process each element in a sequence and build a result) is a pattern called reduce and Clojure has a built-in function for it.

Simple reduce example would be

(reduce + [1 2 3 4 5 6 7 8 9 10])
;=> 55
Enter fullscreen mode Exit fullscreen mode

The reduce works like this:

  • Apply the provided function to the first element(s)
  • Apply the function to the result and next element
  • Repeat until we have elements

It can also take an optional initial value.

(reduce + 10 [1 2 3 4 5 6 7 8 9 10])
;=> 65
Enter fullscreen mode Exit fullscreen mode

We can use reduce to reduce the code of our complete robot function.

(defn complete-robot
 [parts]
 (reduce
 (fn [final part] (into final (set [part (make-right-part part)])))
 []
 parts
 )
)
Enter fullscreen mode Exit fullscreen mode

Count our costs

Now let's make a function that will take a list of parts. Makes right matching parts.
And sums up all the costs.

(defn bill-robot
 [parts]
 (reduce 
 (fn [sum part] (+ sum (:price part)))
 0
 (complete-robot parts)
 )
)

(bill-robot robot-parts)
;=> 220
Enter fullscreen mode Exit fullscreen mode

Yes. It's another example of the reduce pattern.

This brings us to the part where we know all the elementals we need to work with Clojure. Next time we will dive into the core Clojure functions.


You can follow me on Twitter to get more content like this.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .