August 17, 2018

Just Juxt #3: Pack a Sequence (4clojure #31)

Pack a seq with juxt

Write a function which packs consecutive duplicates into sub-lists.

Here's a live testing area for you to play with. See if you can solve it with juxt. Then check out the answer below!

(ns live.test
  (:require [cljs.test :refer-macros [deftest is testing run-tests]]))
  
(defn pack [s]
  )

(deftest test-31
  (is (= (pack [1 1 2 1 1 1 3 3]) '((1 1) (2) (1 1 1) (3 3))))
  (is (= (pack [:a :a :b :b :c]) '((:a :a) (:b :b) (:c))))
  (is (= (pack [[1 2] [1 2] [3 4]]) '(([1 2] [1 2]) ([3 4])))))
  
(run-tests)

Here's how to do it:

(defn pack [[a & z :as s]]
  (when a
    (let [[p q] ((juxt take-while drop-while)
                 #(= a %) s)]
      (cons p (pack q)))))

We will now follow our established pattern of dismantling the fn to see how it works at each step. First by just showing how destructuring works in Clojure:

(let [[a & z :as s]
      [1 1 2 1 1 1 3 3]]
  s)

This binds our seq to a local variable s, and labels the first item a and the rest of the coll z:

(let [[a & z :as s]
      [1 1 2 1 1 1 3 3]]
  a)
(let [[a & z :as s] [1 1 2 1 1 1 3 3]]
  z)

This destructuring pattern is simply a way to call first and rest on our seq and give them all extremely concise names, a, z, and s.

Take a look at the meat of the solution, the juxt. It is used to create one of the let bindings:

((juxt take-while drop-while) #(= 1 %) [1 1 2 1 1 1 3 3])

What's with the double parens? It looks kinda weird, no?

The reason is that juxt returns a fn. Recall its docstring:

user=> (doc juxt)
-------------------------
clojure.core/juxt
([f] [f g] [f g h] [f g h & fs])
  Takes a set of functions and returns a fn that is the juxtaposition
  of those fns.  The returned fn takes a variable number of args, and
  returns a vector containing the result of applying each fn to the
  args (left-to-right).
  ((juxt a b c) x) => [(a x) (b x) (c x)]
((juxt a b c) x) => [(a x) (b x) (c x)]

The juxt returned our vector, [(1 1) (2 1 1 1 3 3)], containing a list with the first 2 dupes of our seq, followed by a list of the remaining items.

The trick is in our predicate fn, #(= a %), which is passed to take-while and drop-while via juxt.

Now for the next operation:

(cons '(1 1) (pack '(2 1 1 1 3 3)))

By way of (when a we are recursively ripping off the chunks of consecutive dupes and pack-ing them into a list! What a juxt-tastic endeavor!

Here we have seen juxt used in a new way, that is, to define a let binding to use destructuring. That's awesome!

And we'll have another party tomorrow! See you later.

Tags: KLIPSE 4clojure Cryogen juxt