How to implement the strategy pattern in Clojure? - clojure

I have three similar functions, they filter a collection of maps matching a key (column) against its value, case-insensitively.
Here is the original code I want to DRY up:
;; gets a collection filtered by a column, exact match
(defn get-collection-by-equals [collection-name column-name column-value]
(filter #(= (string/lower-case (column-name %)) (string/lower-case column-value)) (cached-get-collection collection-name)))
;; gets a collection filtered by a column, with partial match, case-insensitive
(defn get-collection-by-like [collection-name column-name column-value]
(filter #(string/includes? (string/lower-case (column-name %)) (string/lower-case column-value)) (cached-get-collection collection-name)))
;; gets a collection filtered by a column, which starts with given value, case-insensitive
(defn get-collection-by-starts-with [collection-name column-name column-value]
(filter #(string/starts-with? (string/lower-case (column-name %)) (string/lower-case column-value)) (cached-get-collection collection-name)))
You can see how similar the code is, I'm just using a different matching strategy in each case, =, includes? and starts-with?.
My first attempt was as follows:
;; returns a function which does a case-insensitive match between given column and value for the given map
(defn matching-fn [match-fn column-name column-value]
(fn [map] (match-fn (string/lower-case (column-name map)) (string/lower-case column-value))))
;; gets a collection filtered by a column, exact match
(defn get-collection-by-equals [collection-name column-name column-value]
(filter #((matching-fn = column-name column-value) %) (cached-get-collection collection-name)))
;; gets a collection filtered by a column, with partial match, case-insensitive
(defn get-collection-by-like [collection-name column-name column-value]
(filter #((matching-fn string/includes? column-name column-value) %) (cached-get-collection collection-name)))
;; gets a collection filtered by a column, which starts with given value, case-insensitive
(defn get-collection-by-starts-with [collection-name column-name column-value]
(filter #((matching-fn string/starts-with? column-name column-value) %) (cached-get-collection collection-name)))
I didn't like the readability of this solution and it occurred to me that I could just pass the matching function, instead of having a function which returns a function, I came up with this:
;; gets a collection filtered by a column, using the given function, case-insensitive
(defn get-collection-by-filter [collection-name filter-fn column-name column-value]
(filter #(filter-fn (string/lower-case (column-name %)) (string/lower-case column-value)) (cached-get-collection collection-name)))
;; gets a collection filtered by a column, exact match, case-insensitive
(defn get-collection-by-equals [collection-name column-name column-value]
(get-collection-by collection-name = column-name column-value))
;; gets a collection filtered by a column, with partial match, case-insensitive
(defn get-collection-by-like [collection-name column-name column-value]
(get-collection-by collection-name string/includes? column-name column-value))
;; gets a collection filtered by a column, which starts with given value, case-insensitive
(defn get-collection-by-starts-with [collection-name column-name column-value]
(get-collection-by collection-name string/starts-with? column-name column-value))
Is this idiomatic Clojure, are there other (better) solutions?
Using a macro seems overkill.

Like a number of OO patterns, in a functional language the entire pattern boils down to "use a function with an argument". Extract everything but the small part that changes into a new function, with the part that changes as an argument to that function.
(defn collection-comparator [cmp]
(fn [collection-name column-name column-value]
(let [lower-value (string/lower-case column-value)]
(filter #(cmp (string/lower-case (column-name %))
lower-value)
(cached-get-collection collection-name)))))
(def get-collection-by-equals (collection-comparator =))
(def get-collection-by-like (collection-comparator string/includes?))
(def get-collection-by-starts-with (collection-comparator string/starts-with?))

I'd just use your get-collection-by-filter and not wrap it any more—why create a new function for every value the parameter might take?
Aside: just make the description a documentation string, and format it a bit:
(defn get-collection-by-filter
"gets a collection filtered by a column, using the given function,
case-insensitive"
[collection-name filter-fn column-name column-value]
(filter #(filter-fn (string/lower-case (column-name %))
(string/lower-case column-value))
(cached-get-collection collection-name)))

Related

Using swap! instead of reset! in atom - clojure

Given I have this action to perform
(def structure (atom [{:id "an-id"} {:id "another-id"}]))
(def job {:type "some-job"})
(reset! structure (map #(if (= "an-id" (:id %)) (update-in % [:performed-jobs] (fnil conj []) job) %) #structure))
next structure:
[{:id "an-id" :performed-jobs [{:type "some-job"}]} {:id "another-id"}]
How can I use swap! to change a single occurrence in my structure instead of resetting it all?
Replace reset! by swap! by giving it a function that takes the old value of the atom and returns a new value to store in the atom.
Replace dereferencing of the atom with the function's argument, the old value.
(swap! structure
(fn [old]
(map #(if (= "an-id" (:id %))
(update-in % [:performed-jobs]
(fnil conj []) job) %)
old)))

What is the idiomatic way to alter a vector that is stored in an atomized map?

I have an atom called app-state that holds a map. It looks like this:
{:skills [{:id 1 :text "hi"} {:id 2 :text "yeah"}]}
What is the idiomatic way to remove the element inside the vector with :id = 2 ? The result would look like:
{:skills [{:id 1 :text "hi"}]}
...
So far, I have this:
(defn new-list [id]
(remove #(= id (:id %)) (get #app-state :skills)))
swap! app-state assoc :skills (new-list 2)
It works, but I feel like this isn't quite right. I think it could be something like:
swap! app-state update-in [:skills] remove #(= id (:id %))
But this doesn't seem to work.
Any help is much appreciated!
Try this:
(defn new-list [app-state-map id]
(assoc app-state-map :skills (into [] (remove #(= id (:id %)) (:skills app-state-map)))))
(swap! app-state new-list 2)
swap! will pass the current value of the atom to the function you supply it. There's no need to dereference it yourself in the function.
See the docs on swap! for more details.
(swap! state update :skills (partial remove (comp #{2} :id)))
(def skills {:skills [{:id 1 :text "hi"} {:id 2 :text "yeah"}]})
(defn remove-skill [id]
(update skills :skills (fn [sks] (vec (remove #(= id (:id %)) sks)))))
You would then be able to call say (remove-skill 1) and see that only the other one (skill with :id of 2) is left.
I like your way better. And this would need to be adapted for use against an atom.
You can use filter to do this. Here is a function that takes an id and the map and let's you filter out anything that doesn't match your criteria. Of course, you can make the #() reader macro check for equality rather than inequality depending on your needs.
user=> (def foo {:skills [{:id 1 :text "hi"} {:id 2 :text "yeah"}]})
#'user/foo
user=> (defn bar [id sklz] (filter #(not= (:id %) id) (:skills sklz)))
#'user/bar
user=> (bar 1 foo)
({:id 2, :text "yeah"})
user=> (bar 2 foo)
({:id 1, :text "hi"})

Reduce the number of lines of code for recursive search

It's become an obsession of mine to reduce the lines of code that I write in Lisp/Clojure. I am trying to make the following code (essentially a depth first search) shorter.
(defn find-node [nodeid in-node]
(if (= nodeid (:id in-node))
in-node
(loop [node nil activities (:activities in-node)]
(if (or (empty? activities) (not (nil? node)))
node
(recur (find-node nodeid (first activities)) (rest activities))))))
(defn find-node-in-graph [nodeid node activities]
(if (empty? activities)
node
(recur nodeid (find-node nodeid (first activities)) (rest activities))))
(defrecord Graph [id name activities])
(defrecord Node [id name activities])
"activities" is a list
This might be cheating, though it's only one line ;)
(def tree {:id 1 :children [{:id 2 :children [{:id 3}]} {:id 4}]})
core> (filter #(= 3 (:id %)) (tree-seq :children :children tree))
({:id 3})
core> (filter #(= 2 (:id %)) (tree-seq :children :children tree))
({:children [{:id 3}], :id 2})
Some points on the original intent of the question:
loop/recer is often not the most compact form, it wan usually be written with map or doseq
if/recur can often be replaced with a call to filter
If you seem to really need to write something like this because of some specific requirement, often zippers will solve the problem more elegantly.

Clojure Enlive: How to simplify this extract fn from scrape3.clj

I followed the enlive-tutorial and must say i'm impressed by the power of Enlive for parsing the web. Now, I came to look further to the scrape3.clj available here: https://github.com/swannodette/enlive-tutorial/blob/master/src/tutorial/scrape3.clj
Swannodette has made a great job in designing this example, but I feel we could make it a bit dryer.
My question: I would you rewrite this extract function to make it dryer:
(defn extract [node]
(let [headline (first (html/select [node] *headline-selector*))
byline (first (html/select [node] *byline-selector*))
summary (first (html/select [node] *summary-selector*))
result (map html/text [headline byline summary])]
(zipmap [:headline :byline :summary] (map #(re-gsub #"\n" "" %) result))))
If you have other ideas on other elements of the program, feel free to share them!
EDIT:
I played around and came up with:
(defn extract [node]
(let [s [*headline-selector* *byline-selector* *summary-selector*]
selected (map #(html/text (first (html/select [node] %))) s)
cleaned (map #(re-gsub #"\n" "" %) selected)]
(zipmap [:headline :byline :summary] cleaned)))
the first (html/select [node] can be hoisted to a local function:
(defn extract [node]
(let [selector (fn [sel]) (html/select [node] sel)
headline (selector *headline-selector*)
byline (selector *byline-selector*)
summary (selector *summary-selector*)
result (map html/text [headline byline summary])]
(zipmap [:headline :byline :summary] (map #(re-gsub #"\n" "" %) result))))
then the intermediary names can be removed, though these help make the point of the code clear so it's a matter of personal taste:
(defn extract [node]
(let [selector (fn [selector]) (html/select [node] selector)
result (map html/text
(map selector [*headline-selector*
*byline-selector*
*summary-selector*]))]
(zipmap [:headline :byline :summary] (map #(re-gsub #"\n" "" %) result))))
To make the result of the function "more visible" I would use map literal as shown below:
(defn extract [node]
(let [sel #(html/text (first (html/select [node] %)))
rem #(re-gsub #"\n" "" %)
get-text #(-> % sel rem)]
{:headline (get-text *headline-selector*)
:byline (get-text *byline-selector*)
:summary (get-text *summary-selector*)
}))

clojure set of maps - basic filtering

Clojure beginner here..
If I have a set of maps, such as
(def kids #{{:name "Noah" :age 5}
{:name "George":age 3}
{:name "Reagan" :age 1.5}})
I know I can get names like this
(map :name kids)
1) How do I select a specific map? For example
I want to get back the map where name="Reagan".
{:name "Reagan" :age 1.5}
Can this be done using a filter?
2) How about returning the name where age = 3?
Yes, you can do it with filter:
(filter #(= (:name %) "Reagan") kids)
(filter #(= (:age %) 3) kids)
There's clojure.set/select:
(clojure.set/select set-of-maps #(-> % :age (= 3)))
And similarly with name and "Reagan". The return value in this case will be a set.
You could also use filter without any special preparations, since filter calls seq on its collection argument (edit: as already described by ffriend while I was typing this):
(filter #(-> % :age (= 3))) set-of-maps)
Here the return value will be a lazy seq.
If you know there will only be one item satisfying your predicate in the set, some will be more efficient (as it will not process any additional elements after finding the match):
(some #(if (-> % :age (= 3)) %) set-of-maps)
The return value here will be the matching element.