Brad Lucas

Programming, Clojure and other interests
June 13, 2017

Juxt

Overview

While cleaning out some old Gists I found a function I wrote six years ago using Clojure's juxt function.

The gist is here:

https://gist.github.com/bradlucas/1233763

To save a click I'll put the function here after fixing the embarassing typo in the comment.

(defn find-new-members
  "Return the new people who are only in the new-list but not in the current-list.
Both lists are lists of maps where they have at least :first_name and :last_name keys.
"
  [current-list new-list]
  (let [existing (set (remove #(nil? (second %)) (map (juxt :first_name :last_name) current-list)))]
    (remove #(existing ((juxt :first_name :last_name) %)) new-list)))

Comments

In reviewing this I remember why I wrote it. I was building some functions to process email lists. The scenario was that there was a current saved list which needed to periodically updated upon receiving a new list. Often the new list was very similar to the old or current list save for a few updates. Identifying the new members from the new list was the purpose of the function.

I notice that in my function I was also concerned about empty last name values in the current list. It looks like I wanted to ignore them for consideration but I wasn't concerned about empty last names in the new list. I'm not sure why.

juxt

So, juxt is interesting because it takes one or more functions and returns a function that returns a vector of results from appying each function to it's arguments. Here the idea is to apply :firstname and :lastname to each item in a list. This returns vector pairs with the firstname and lastname valuee.

For example, if the current list contains [{:firstname "A" :lastname "B"}] we'd expect (map (juxt :firstname :lastname) [{:firstname "A" :lastname "B"}]) to return (["A" "B"]).

Rewrite

Myabe this function can be improved. In addition to ignoring empty last name items I think there is redundancy in the map juxt bits. That looks like there should be a function there.

(defn find-new-members
  "Return the new people who are only in the new-list but not in the current-list.
Both lists are lists of maps where they have at least :first_name and :last_name keys.
"
  [current-list new-list]

  ;; build a set of all unique first_name, last_name pairs from a list of {:first_name :last_name} maps
  ;; while building the set remove any items with a missing last_name
  
  (let [build-pair-set (fn [lst] (set (remove #(nil? (second %)) (map (juxt :first_name :last_name) lst))))
  
        current (build-pair-set current-list)
        new      (build-pair-set new-list)]

    ;; remove all pairs from the new list if they are in the current list
    (remove #(current %) new)))

Shorten

Removing the commments and the current and new variables results in this updated function.

(defn find-new-members
  "Return the new people who are only in the new-list but not in the current-list.
Both lists are lists of maps where they have at least :first_name and :last_name keys.
"
  [current-list new-list]
  (let [build-pair-set (fn [lst] (set (remove #(nil? (second %)) (map (juxt :first_name :last_name) lst))))]
    (remove #((build-pair-set current-list) %) (build-pair-set new-list))))

Testing with an example

Here are two lists to test with. The new list has a row which is new to the current list. This is the one we should see when we run the find-new-members list.

(def current-list
  [
   {:first_name "A" :last_name "B"}
   {:first_name "C" :last_name "D"}
   {:first_name "A" :last_name "B"}
   ])
(def new-list
  [
   {:first_name "A" :last_name "B"}
   {:first_name "E" :last_name "F"}
   ])


(println (find-new-members current-list new-list))

You should see the following:

juxt.core> (println (find-new-members current-list new-list))
([E F])
nil
Tags: clojure