Skip to main content

Functional Toolbox

Progress 0/16

This is where Phel sings. map, filter, reduce, threading macros, list comprehensions, and destructuring turn data wrangling into a readable pipeline.

Exercise 1 medium

Increment every number in [4 7 9 10]. Use map.

;; pass a named function directly
(map inc [4 7 9 10])
;; => @[5 8 10 11]

;; or with the short anonymous form
(map #(+ % 1) [4 7 9 10])

;; or with fn
(map (fn [x] (+ x 1)) [4 7 9 10])

map applies a function to every element and returns a new collection. When a built-in already does what you need (like inc), pass it by name.

Learn more: Functions and Recursion

Exercise 2 medium

Uppercase every name:

["ada" "grace" "alan"]
;; => ["ADA" "GRACE" "ALAN"]

Hint: php/strtoupper does the lifting.

(map php/strtoupper ["ada" "grace" "alan"])
;; => @["ADA" "GRACE" "ALAN"]

PHP functions are reachable via the php/ prefix - the entire PHP stdlib is at your fingertips.

Learn more: PHP Interop

Exercise 3 medium

Keep only the even numbers in [1 2 3 4 5 6 7 8 9 10].

(filter even? [1 2 3 4 5 6 7 8 9 10])
;; => @[2 4 6 8 10]

filter keeps the elements for which the predicate returns truthy.

Exercise 4 medium

From [1 2 3 4 5 6 7 8 9 10], keep the evens and double each.

(map #(* % 2) (filter even? [1 2 3 4 5 6 7 8 9 10]))
;; => @[4 8 12 16 20]

Composing filter and map is a daily pattern. Read it inside-out: filter first, then map.

Exercise 5 medium

Rewrite the previous solution with the threading macro ->> so it reads top-to-bottom.

(->> [1 2 3 4 5 6 7 8 9 10]
     (filter even?)
     (map #(* % 2)))
;; => @[4 8 12 16 20]

->> (thread-last) feeds each result as the last argument to the next call. Nested calls become a clear pipeline.

Learn more: Functions and Recursion

Exercise 6 medium

Use the other threading macro, ->, to build a user step by step:

(-> {}
    (assoc :name "Ada")
    (assoc :age 36)
    (assoc :role :admin))
;; => {:name "Ada" :age 36 :role :admin}
(-> {}
    (assoc :name "Ada")
    (assoc :age 36)
    (assoc :role :admin))
;; => {:name "Ada" :age 36 :role :admin}

-> (thread-first) inserts each result as the first argument of the next call. Use -> for "build up a value" pipelines (often with maps), and ->> for "transform a sequence" pipelines.

Learn more: Functions and Recursion

Exercise 7 medium

Use reduce to sum [1 2 3 4 5].

(reduce + 0 [1 2 3 4 5])
;; => 15

reduce collapses a collection to a single value: it takes a function, an initial value, and a collection. Here it computes (+ (+ (+ (+ (+ 0 1) 2) 3) 4) 5).

Exercise 8 medium

Use reduce to find the longest string in ["cat" "elephant" "dog" "hippopotamus"].

(reduce
  (fn [longest s]
    (if (> (php/strlen s) (php/strlen longest)) s longest))
  ""
  ["cat" "elephant" "dog" "hippopotamus"])
;; => "hippopotamus"

Any time you need to fold a collection down to a single value, reduce is the tool.

Exercise 9 medium

Use for to extract every :value from this vector of maps:

(def data [{:id 1 :value 10.3} {:id 2 :value 20.06} {:id 7 :value 30.1}])

Expected result: (10.3 20.06 30.1).

(def data [{:id 1 :value 10.3} {:id 2 :value 20.06} {:id 7 :value 30.1}])
(for [m :in data] (:value m))
;; => @[10.3 20.06 30.1]

for is a list comprehension - it generates a new sequence by transforming each element.

Learn more: Control Flow

Exercise 10 medium

Use for with two bindings to generate every combination of suit and rank:

(def suits [:hearts :diamonds :clubs :spades])
(def ranks [:ace :king :queen])
;; => build all 12 [suit rank] pairs
(def suits [:hearts :diamonds :clubs :spades])
(def ranks [:ace :king :queen])

(for [s :in suits
      r :in ranks]
  [s r])
;; => @[[:hearts :ace] [:hearts :king] [:hearts :queen]
;;      [:diamonds :ace] [:diamonds :king] [:diamonds :queen]
;;      [:clubs :ace] [:clubs :king] [:clubs :queen]
;;      [:spades :ace] [:spades :king] [:spades :queen]]

Multiple bindings act like nested loops: the right-most binding varies fastest. Add :when clauses to filter, :let clauses to bind locals.

Learn more: Control Flow

Exercise 11 medium

Sort a list of people by age:

(def people [{:name "Charlie" :age 30}
             {:name "Ada" :age 36}
             {:name "Bob" :age 25}])
(def people [{:name "Charlie" :age 30}
             {:name "Ada" :age 36}
             {:name "Bob" :age 25}])

(sort-by :age people)
;; => [{:name "Bob" :age 25} {:name "Charlie" :age 30} {:name "Ada" :age 36}]

sort-by takes a key function and a collection. Since keywords are functions, :age extracts the value to compare on.

Exercise 12 medium

Use update-in to bump the balance from 3 to 4:

(def data {:shops [:shop-1]
           :customers [{:id "Bob"
                        :account {:balance 3}}]})
(def data {:shops [:shop-1]
           :customers [{:id "Bob"
                        :account {:balance 3}}]})

(update-in data [:customers 0 :account :balance] inc)
;; => {:shops [:shop-1] :customers [{:id "Bob" :account {:balance 4}}]}

update-in walks into a nested structure and applies a function at the path.

Learn more: Data Structures

Exercise 13 medium

Use frequencies to count fruit appearances:

["apple" "banana" "apple" "cherry" "banana" "apple"]
(frequencies ["apple" "banana" "apple" "cherry" "banana" "apple"])
;; => {"apple" 3 "banana" 2 "cherry" 1}

frequencies returns a map from each distinct element to its count. Perfect for tallies.

Exercise 14 medium

Use group-by to split numbers into evens and odds:

[1 2 3 4 5 6 7 8]
(group-by even? [1 2 3 4 5 6 7 8])
;; => {false [1 3 5 7] true [2 4 6 8]}

group-by runs the function on each element and bundles values that share a result.

Exercise 15 hard

Define area so it accepts a map {:width w :height h} and returns w * h. Use destructuring in the parameter list:

(area {:width 5 :height 3}) ; => 15
(defn area [{:keys [width height]}]
  (* width height))

{:keys [width height]} destructures the map directly in the parameter list - no let required. This pattern is everywhere in idiomatic Phel: tidy callers, self-documenting signatures.

Learn more: Destructuring

Exercise 16 hard

Define combine that merges a vector of maps into one:

(combine [{:a 1 :b 2} {:c 3} {:d 4 :e 5}])
;; => {:a 1 :b 2 :c 3 :d 4 :e 5}
(defn combine [maps]
  (apply merge maps))

apply "spreads" the vector as individual arguments. So (apply merge [{:a 1} {:b 2}]) is the same as (merge {:a 1} {:b 2}). This trick works with any variadic function.

Learn more: Functions and Recursion