Bobbi Towers / Jun 26 2019

Clojure Brave Chapter 4

Seq Function Examples

map

(map inc [1 2 3])
(map str ["a" "b" "c"] ["A" "B" "C"])

(def human-consumption   [8.1 7.3 6.6 5.0])
(def critter-consumption [0.0 0.2 0.3 1.1])
(defn unify-diet-data
  [human critter]
  {:human human
   :critter critter})

(map unify-diet-data human-consumption critter-consumption)
List(4) (Map, Map, Map, Map)
(def sum #(reduce + %))
(def avg #(/ (sum %) (count %)))
(defn stats
  [numbers]
  (map #(% numbers) [sum count avg]))

(stats [3 4 10])
List(3) (17, 3, Vector(2))
(def identities
  [{:alias "Batman" :real "Bruce Wayne"}
   {:alias "Spider-Man" :real "Peter Parker"}
   {:alias "Santa" :real "Your mom"}
   {:alias "Easter Bunny" :real "Your dad"}])

(map :real identities)
List(4) ("Bruce Wayne", "Peter Parker", "Your mom", "Your dad")

reduce

Chapter 3 showed how reduce processes each element in a sequence to build a result. This section shows a couple of other ways to use it that might not be obvious.The first use is to transform a map’s values, producing a new map with the same keys but with updated values:

(reduce (fn [new-map [key val]]
          (assoc new-map key (inc val)))
        {}
        {:max 30 :min 10})
Map {:max: 31, :min: 11}

In this example, reduce treats the argument {:max 30 :min 10} as a sequence of vectors, like ([:max 30] [:min 10]). Then, it starts with an empty map (the second argument) and builds it up using the first argument, an anonymous function. It’s as if reduce does this:

(assoc (assoc {} :max (inc 30))
       :min (inc 10))
Map {:max: 31, :min: 11}

The function assoc takes three arguments: a map, a key, and a value. It derives a new map from the map you give it by associating the given key with the given value. For example, (assoc {:a 1} :b 2) would return {:a 1 :b 2}

Another use for reduce is to filter out keys from a map based on their value. In the following example, the anonymous function checks whether the value of a key-value pair is greater than 4. If it isn’t, then the key-value pair is filtered out. In the map {:human 4.1 :critter 3.9}, 3.9 is less than 4, so the :critter key and its 3.9 value are filtered out.

(reduce (fn [new-map [key val]]
          (if (> val 4)
            (assoc new-map key val)
            new-map))
        {}
        {:human 4.1
         :critter 3.9})
Map {:human: 4.1}

The takeaway here is that reduce is a more flexible function than it first appears. Whenever you want to derive a new value from a seqable data structure, reduce will usually be able to do what you need. If you want an exercise that will really blow your hair back, try implementing map using reduce, and then do the same for filter and some after you read about them later in this chapter.

take, drop, take-while, and drop-while

(def food-journal
  [{:month 1 :day 1 :human 5.3 :critter 2.3}
   {:month 1 :day 2 :human 5.1 :critter 2.0}
   {:month 2 :day 1 :human 4.9 :critter 2.1}
   {:month 2 :day 2 :human 5.0 :critter 2.5}
   {:month 3 :day 1 :human 4.2 :critter 3.3}
   {:month 3 :day 2 :human 4.0 :critter 3.8}
   {:month 4 :day 1 :human 3.7 :critter 3.9}
   {:month 4 :day 2 :human 3.7 :critter 3.6}])

(take-while #(< (:month %) 3) food-journal)
(drop-while #(< (:month %) 3) food-journal)
(take-while #(< (:month %) 4)
            (drop-while #(< (:month %) 2) food-journal))
List(4) (Map, Map, Map, Map)

filter and some

(filter #(< (:human %) 5) food-journal)
(filter #(< (:month %) 3) food-journal)
List(4) (Map, Map, Map, Map)
(some #(> (:critter %) 5) food-journal)
(some #(> (:critter %) 3) food-journal)
(some #(and (> (:critter %) 3) %) food-journal)
Map {:month: 3, :day: 1, :human: 4.2, :critter: 3.3}

sort and sort-by

(sort [3 1 2])
(sort-by count ["aaa" "c" "bb"])
List(3) ("c", "bb", "aaa")

concat

(concat [1 2] [3 4])
List(4) (1, 2, 3, 4)

Lazy Seqs

Demonstrating Lazy Seq Efficiency

(def vampire-database
  {0 {:makes-blood-puns? false, :has-pulse? true  :name "McFishwich"}
   1 {:makes-blood-puns? false, :has-pulse? true  :name "McMackson"}
   2 {:makes-blood-puns? true,  :has-pulse? false :name "Damon Salvatore"}
   3 {:makes-blood-puns? true,  :has-pulse? true  :name "Mickey Mouse"}})

(defn vampire-related-details
  [social-security-number]
  (Thread/sleep 1000)
  (get vampire-database social-security-number))

(defn vampire?
  [record]
  (and (:makes-blood-puns? record)
       (not (:has-pulse? record))
       record))

(defn identify-vampire
  [social-security-numbers]
  (first (filter vampire?
                 (map vampire-related-details social-security-numbers))))

(time (vampire-related-details 0))
Map {:makes-blood-puns?: false, :has-pulse?: true, :name: "McFishwich"}
(time (def mapped-details (map vampire-related-details (range 0 1000000))))
user/mapped-details
(time (first mapped-details))
Map {:makes-blood-puns?: false, :has-pulse?: true, :name: "McFishwich"}
(time (first mapped-details))
Map {:makes-blood-puns?: false, :has-pulse?: true, :name: "McFishwich"}
(time (identify-vampire (range 0 1000000)))
Map {:makes-blood-puns?: true, :has-pulse?: false, :name: "Damon Salvatore"}

Infinite Sequences

(concat (take 8 (repeat "na")) ["Batman!"])
List(9) ("na", "na", "na", "na", "na", "na", "na", "na", "Batman!")
(take 3 (repeatedly (fn [] (rand-int 10))))
List(3) (9, 5, 5)
(defn even-numbers
  ([] (even-numbers 0))
  ([n] (cons n (lazy-seq (even-numbers (+ n 2))))))

(take 10 (even-numbers))
List(10) (0, 2, 4, 6, 8, 10, 12, 14, 16, 18)

The Collection Abstraction

(map identity {:sunlight-reaction "Glitter!"})
List(1) (Vector(2))
(into {} (map identity {:sunlight-reaction "Glitter!"}))
Map {:sunlight-reaction: "Glitter!"}
(map identity [:garlic :sesame-oil :fried-eggs])
List(3) (:garlic, :sesame-oil, :fried-eggs)
(into [] (map identity [:garlic :sesame-oil :fried-eggs]))
Vector(3) [:garlic, :sesame-oil, :fried-eggs]
(into #{} (map identity [:garlic-clove :garlic-clove]))
Set(1) #{:garlic-clove}
(into {:favorite-emotion "gloomy"} [[:sunlight-reaction "Glitter!"]])
Map {:favorite-emotion: "gloomy", :sunlight-reaction: "Glitter!"}
(into ["cherry"] '("pine" "spruce"))
Vector(3) ["cherry", "pine", "spruce"]
(into {:favorite-animal "kitty"} {:least-favorite-smell "dog"
                                  :relationship-with-teenager "creepy"})
Map {:favorite-animal: "kitty", :least-favorite-smell: "dog", :relationship-with-teenager: "creepy"}

Function Functions

apply

(apply max [0 1 2])
2
(defn my-into
  [target additions]
  (apply conj target additions))

(my-into [0] [1 2 3])
Vector(4) [0, 1, 2, 3]

partial

partial takes a function and any number of arguments. It then returns a new function. When you call the returned function, it calls the original function with the original arguments you supplied it along with the new arguments.

(def add10 (partial + 10))
(add10 3)
(add10 5)
15
(def add-missing-elements
  (partial conj ["water" "earth" "air"]))

(add-missing-elements "unobtainium" "adamantium")
Vector(5) ["water", "earth", "air", "unobtainium", "adamantium"]

So when you call add10, it calls the original function and arguments (+ 10) and tacks on whichever arguments you call add10 with. To help clarify how partial works, here’s how you might define it:

(defn my-partial
  [partialized-fn & args]
  (fn [& more-args]
    (apply partialized-fn (into args more-args))))

(def add20 (my-partial + 20))
(add20 3) 
23
(defn lousy-logger
  [log-level message]
  (condp = log-level
    :warn (clojure.string/lower-case message)
    :emergency (clojure.string/upper-case message)))

(def warn (partial lousy-logger :warn))

(warn "Red light ahead")
"red light ahead"

complement

(defn my-complement
  [fun]
  (fn [& args]
    (not (apply fun args))))

(def my-pos? (complement neg?))
(my-pos? 1)
(my-pos? -1)
false
(defn identify-humans
  [social-security-numbers]
  (filter #(not (vampire? %))
          (map vampire-related-details social-security-numbers)))

(def not-vampire? (complement vampire?))
(defn identify-humans
  [social-security-numbers]
  (filter not-vampire?
          (map vampire-related-details social-security-numbers)))
user/identify-humans

A Vampire Data Analysis Program for the FWPD

echo 'Edward Cullen,10
Bella Swan,0
Charlie Swan,0
Jacob Black,3
Carlisle Cullen,6' > suspects.csv
(def filename "suspects.csv")
(slurp filename)
"Edward Cullen,10 Bella Swan,0 Charlie Swan,0 Jacob Black,3 Carlisle Cullen,6 "
(def vamp-keys [:name :glitter-index])

(defn str->int
  [str]
  (Integer. str))

(def conversions {:name identity
                  :glitter-index str->int})

(defn convert
  [vamp-key value]
  ((get conversions vamp-key) value))

(convert :glitter-index "3")
3
(defn parse
  "Convert a CSV into rows of columns"
  [string]
  (map #(clojure.string/split % #",")
       (clojure.string/split string #"\n")))
user/parse
(parse (slurp filename))
List(5) (Vector(2), Vector(2), Vector(2), Vector(2), Vector(2))
(defn mapify
  "Return a seq of maps like {:name \"Edward Cullen\" :glitter-index 10}"
  [rows]
  (map (fn [unmapped-row]
         (reduce (fn [row-map [vamp-key value]]
                   (assoc row-map vamp-key (convert vamp-key value)))
                 {}
                 (map vector vamp-keys unmapped-row)))
       rows))

(first (mapify (parse (slurp filename))))
Map {:name: "Edward Cullen", :glitter-index: 10}
(defn glitter-filter
  [minimum-glitter records]
  (filter #(>= (:glitter-index %) minimum-glitter) records))

(glitter-filter 3 (mapify (parse (slurp filename))))
List(3) (Map, Map, Map)