✓ Functional Sequences Transformations

Sequeces in Clojure

Sequences are everywhere in Clojure. It’s a powerful abstract over many different data structures and programming scenarios.

Lists and vectors are sequences

'(:a :b :c)
(:a :b :c)
[1 2 3]
[1 2 3]

A few functions to process sequences

(let [my-list '(:a :b :c)]
    (first my-list))
:a
(let [my-list '(:a :b :c)]
    (last my-list))
:c
(let [my-vec [1 2 3]]
    (first my-vec))
1
(let [my-vec [1 2 3]]
    (last my-vec))
3
(let [my-vec [1 2 3]]
    (take 2 my-vec))
(1 2)

Hash-maps are sequences too

Hash-maps can also be used as a sequence. It’s a sequence of [key value] pairs. So, each element is a vector of length two consisting of the key and the value.

(let [my-hashmap {:a 1
                  :b 2
                  :c 3}]
    (first my-hashmap))
[:a 1]
(let [my-hashmap {:a 1
                  :b 2
                  :c 3}]
    (last my-hashmap))
[:c 3]
(let [my-hashmap {:a 1
                  :b 2
                  :c 3}]
    (take 2 my-hashmap))
([:a 1] [:b 2])

Review of for-iteration

The for-form always evaluates to a sequence. The for-form generates the elements by iterating over one or more input sequences.

General form

(for [ <symbol> <sequence>
       <symbol> <sequence>
       ...
       :when <condition>
       :while <condition>
       :let [binding...]]
    <expression>)
(for [x [:a :b :c]
      y [1 2 3]]
    [x y])
([:a 1] [:a 2] [:a 3] [:b 1] [:b 2] [:b 3] [:c 1] [:c 2] [:c 3])

Why use for-form?

The for-form is similar to the more traditional for-loops in Python, and thus are easier to read for most people.

Why not use the for-form?

For-form is not friendly to composition.

Consider the scenario of:

  1. The first for-form generates a sequence.

  2. Then a second for-form process the elements from the first for-form

(defn format-elements [x y]
    (str "x=" x " and y=" y))
#'user/format-elements
(for [elem (for [x [:a :b :c]
                 y [1  2]]
               [x y])]
    (apply format-elements elem))
("x=:a and y=1" "x=:a and y=2" "x=:b and y=1" "x=:b and y=2" "x=:c and y=1" "x=:c and y=2")
(for [string (for [elem (for [x [:a :b :c]
                              y [1 2]]
                            [x y])]
                 (apply format-elements elem))]
    (clojure.string/upper-case string))
("X=:A AND Y=1" "X=:A AND Y=2" "X=:B AND Y=1" "X=:B AND Y=2" "X=:C AND Y=1" "X=:C AND Y=2")

Functional programming

Functions are composable.

Using higher order functions, we have interesting ways to describe sequence transformations.

Map

About map

  • map is not a form. It’s is a function.

  • The signature of the map function is:

Note

See: https://clojuredocs.org/clojure.core/map

(map <function> <sequence>)
(map <function> <sequence-1> <sequence-2>)
(map <function> <sequence-1> <sequence-2> <sequence-3>)
  • The function is called the mapping function, or sometimes the mapper.

  • The map function returns a (lazy) sequence.

Map action

The elements from input sequences are used as input arguments to the mapping function. Elements from the input sequences are matched up until one of the sequence is exhausted.

The return values of the mapping function form the output sequence.

Adding numbers pairwise

(let [xs [1 2 3]
      ys [4 5 6 7 8]]
    (map + xs ys))
(5 7 9)
(let [xs [1 2 3]
      ys [4 5 6]]
    (map vector xs ys))
([1 4] [2 5] [3 6])

Revisiting iterations

Note: map is not equivalent to for-form when applied to multiple sequences.

(for [x [:a :b :c]
      y [1 2]]
    [x y])
([:a 1] [:a 2] [:b 1] [:b 2] [:c 1] [:c 2])
(let [xs [:a :b :c]
      ys [1 2]]
    (map vector xs ys))
([:a 1] [:b 2])

Map is composable

(map format-elements [:a :b :c] [1 2])
("x=:a and y=1" "x=:b and y=2")
(map clojure.string/upper-case
     (map format-elements [:a :b :c] [1 2]))
("X=:A AND Y=1" "X=:B AND Y=2")

Map composition using threading

(->> (map format-elements [:a :b :c] [1 2])
     (map clojure.string/upper-case))
("X=:A AND Y=1" "X=:B AND Y=2")

Filter

About filter

Note

(filter <predicate> <sequence>)
  • Recall a predicate is a function that returns true or false.

  • The filter function returns a sequence from the input sequence that evaluate to true by the predicate function.

Example

(filter #(< 4 (count %))
        ["hello"
         "world"
         "again"
         "and"
         "again"])
("hello" "world" "again" "again")

Filter is composable

(filter #(clojure.string/includes? % "a")
        ["hello"
         "world"
         "again"
         "and"
         "again"])
("again" "and" "again")
(filter #(< 4 (count %))
  (filter #(clojure.string/includes? % "a")
          ["hello"
           "world"
           "again"
           "and"
           "again"]))
("again" "again")

Threading and composition

(->> ["hello"
      "world"
      "again"
      "and"
      "again"]
    (filter #(< 4 (count %)))
    (filter #(clojure.string/includes? % "a")))
("again" "again")

Reduce

Reduce is the most powerful form of functional sequence processing.

About reduce

Note

(reduce <reducer-fn> <initial-value> <sequence>)

where the reducer-fn has the signature of:

(<reducer-fn> <state-value> <input-element>)

Unlike map and filter, reduce can produce data value of any type.

It uses a function, known as the reducer, to iteratively transform a state value with the elements in the input sequence.

Reduce action

During the reduce iteration, we starts with a state value being the initial-value.

For each element in the sequence, a new state value is computed as:

(<reducer-fn> state-value element)

Reducer explained

Suppose we have an input sequence:

["hello" "world" "again" "and" "again"]

We may want to compute the total characters.

  • Use an integer as the state value, with initial value set to 0.

  • Use a reducer function to compute the new state value for each input element.

  • The total characters can be computed using reduce.

(fn [total string] (+ total (count string)))
#function[user/eval5902/fn--5903]
(reduce (fn [total string] (+ total (count string)))
        0
        ["hello" "world" "again" "and" "again"])
23

Another example of reduce

Adding a sequence of numbers using reduce can be done using + as the reducer function.

(reduce + 0 (range 1000000))
499999500000

Composability of map/filter/reduce

How many characters are there in total for strings that start with “a” in the sequence

["hello" "world" "again" "and" "again"]
(->> ["hello" "world" "again" "and" "again"]
    (filter #(clojure.string/starts-with? % "a")))
("again" "and" "again")
(->> ["hello" "world" "again" "and" "again"]
    (filter #(clojure.string/starts-with? % "a"))
    (map count))
(5 3 5)
(->> ["hello" "world" "again" "and" "again"]
    (filter #(clojure.string/starts-with? % "a"))
    (map count)
    (reduce + 0))
13

Summary

  • Functional programming prompts abstraction by exploring patterns that involve higher order functions.

  • Map/filter/reduce functions allow very succint and composable code.

  • Clojure provides different programming constructs from low-level loop/recur to sequence iteration with for-forms, to the higher level abstraction using map/filter/reduce.