Clojure Brave Chapter 4
Seq Function Examples
map
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)
(def sum (reduce + %)) (def avg (/ (sum %) (count %))) (defn stats [numbers] (map (% numbers) [sum count avg])) (stats [3 4 10])
(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)
reduce
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})
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))
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})
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
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))
filter
and some
filter
and some
(filter (< (:human %) 5) food-journal) (filter (< (:month %) 3) food-journal)
(some (> (:critter %) 5) food-journal) (some (> (:critter %) 3) food-journal) (some (and (> (:critter %) 3) %) food-journal)
sort
and sort-by
sort
and sort-by
(sort [3 1 2]) (sort-by count ["aaa" "c" "bb"])
concat
concat
(concat [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))
(time (def mapped-details (map vampire-related-details (range 0 1000000))))
(time (first mapped-details))
(time (first mapped-details))
(time (identify-vampire (range 0 1000000)))
Infinite Sequences
(concat (take 8 (repeat "na")) ["Batman!"])
(take 3 (repeatedly (fn [] (rand-int 10))))
(defn even-numbers ([] (even-numbers 0)) ([n] (cons n (lazy-seq (even-numbers (+ n 2)))))) (take 10 (even-numbers))
The Collection Abstraction
(map identity {:sunlight-reaction "Glitter!"})
(into {} (map identity {:sunlight-reaction "Glitter!"}))
(map identity [:garlic :sesame-oil :fried-eggs])
(into [] (map identity [:garlic :sesame-oil :fried-eggs]))
(into {} (map identity [:garlic-clove :garlic-clove]))
(into {:favorite-emotion "gloomy"} [[:sunlight-reaction "Glitter!"]])
(into ["cherry"] ("pine" "spruce"))
(into {:favorite-animal "kitty"} {:least-favorite-smell "dog" :relationship-with-teenager "creepy"})
Function Functions
apply
apply
(apply max [0 1 2])
(defn my-into [target additions] (apply conj target additions)) (my-into [0] [1 2 3])
partial
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)
(def add-missing-elements (partial conj ["water" "earth" "air"])) (add-missing-elements "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)
(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")
complement
complement
(defn my-complement [fun] (fn [& args] (not (apply fun args)))) (def my-pos? (complement neg?)) (my-pos? 1) (my-pos? -1)
(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)))
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)
(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")
(defn parse "Convert a CSV into rows of columns" [string] (map (clojure.string/split % ",") (clojure.string/split string "\n")))
(parse (slurp filename))
(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))))
(defn glitter-filter [minimum-glitter records] (filter (>= (:glitter-index %) minimum-glitter) records)) (glitter-filter 3 (mapify (parse (slurp filename))))