Using swap! on a nested map - clojure

I'm following Chapter 6 of the book Professional Clojure.
App state is currently defined as follows:
(defonce app-state
(reagent/atom
{:projects
{"aaa"
{:title "Build Whip"
:stories
{1 {:title "Design a data model for projects and stories"
:status "done"
:order 1}
2 {:title "Create a story title entry form"
:order 2}
3 {:title "Implement a way to finish stories"
:order 3}}}}}))
I need to use swap! to add a new key value to represent a new story, keyed by an id with value of the given fields.
(defn add-story! [app-state project-id title status] ;
; Q. How to use swap! to add a key value pair into :stories?
(swap! app-state update [:projects project-id :stories] assoc <- INCORRECT CODE HERE
(unique) {:title title
:status status
:order (inc (max-order app-state project-id))}))
The unique function not shown here just generates any unique uuid.
The max-order function gets, well, the max order.
I had to modify it as the book chapter proceeds inconsistently with the actual final code supplied. Here's my version of max-order:
(defn max-order [app-state project-id]
(apply max 0 (map :order (vals (get-in #app-state [:projects project-id :stories])))))
Question: How can I use swap! to add a new key value into :stories?
I had a go at it but it beat me for now.
I do get the feeling this nested map is not the best representation - in the final code supplied as a download the author has changed to a more relational-type model with projects and stories both as top level entities, with stories containing a project_id, but would be nice to solve this first use of swap! before moving on to that.

I think you can just use assoc-in in this case which is a bit simpler than update-in and better describes what you're trying to achieve:
(def app-state
(atom
{:projects
{"aaa"
{:title "Build Whip"
:stories {1 {:title "Design a data model for projects and stories"
:status "done"
:order 1}
2 {:title "Create a story title entry form"
:order 2}
3 {:title "Implement a way to finish stories"
:order 3}}}}}))
(defn unique [] (rand-int 1000000000))
(let [unique-key (unique)]
(swap! app-state
assoc-in
[:projects "aaa" :stories unique-key]
{:title (str "Agent " unique-key)
:status "foxy"
:order "some-order-stuff"}))
#app-state
;; => {:projects
;; {"aaa"
;; {:title "Build Whip",
;; :stories
;; {1 {:title "Design a data model for projects and stories", :status "done", :order 1},
;; 2 {:title "Create a story title entry form", :order 2},
;; 3 {:title "Implement a way to finish stories", :order 3},
;; 295226401 {:title "Agent 295226401", :status "foxy", :order "some-order-stuff"}}}}}

Be sure to study the Clojure CheatSheet daily! Also, more documentation references here.
I think you just need to use update-in
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require
[clojure.string :as str]
))
(def state
{:projects
{"aaa"
{:title "Build Whip"
:stories {1 {:title "Design a data model for projects and stories"
:status "done"
:order 1}
2 {:title "Create a story title entry form"
:order 2}
3 {:title "Implement a way to finish stories"
:order 3}}}}})
(dotest
(let [result (update-in state [:projects "aaa" :stories]
assoc 99 {:title "Agent 99"
:status "foxy"
:order "some-order-stuff"})
]
(spyx-pretty result)))
with result:
-------------------------------
Clojure 1.10.1 Java 13
-------------------------------
Testing tst.demo.core
result =>
{:projects
{"aaa"
{:title "Build Whip",
:stories
{1
{:title "Design a data model for projects and stories",
:status "done",
:order 1},
2 {:title "Create a story title entry form", :order 2},
3 {:title "Implement a way to finish stories", :order 3},
99 {:title "Agent 99", :status "foxy", :order "some-order-stuff"}}}}}
When the data is inside an atom, the solution is nearly the same:
(def state-atom (atom state))
(let [result (swap! state-atom update-in [:projects "aaa" :stories]
assoc 99 {:title "Agent 99"
:status "foxy"
:order "some-order-stuff"})]
(spyx-pretty #state-atom))
with result
(clojure.core/deref state-atom) =>
{:projects
{"aaa"
{:title "Build Whip",
:stories
{1
{:title "Design a data model for projects and stories",
:status "done",
:order 1},
2 {:title "Create a story title entry form", :order 2},
3 {:title "Implement a way to finish stories", :order 3},
99
{:title "Agent 99", :status "foxy", :order "some-order-stuff"}}}}}

Related

Get list of values from vector of hashmap using filter or for

I'm quite new to clojure and have been struggling to understand how things work exactly. I have a vector of hashmaps as such, titled authors:
------ Authors -----------
[{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}]
I want to write a function that takes the id, and returns a list of the corresponding author names. I have two options for doing so: using filter or using for loop.
When using filter, I have a predicate function already that returns true if the author has matching id:
(defn check-by-id [author id]
(if (= id (:id author)) true false))
But I'm not sure how to use this in order to get the list of author names when passing the id.
Three other ways via keep, for and reduce:
(keep (fn [{:keys [id name]}] (when (= id 100) name)) authors)
;; => ("Albert Einstein")
(for [{:keys [id name]} authors
:when (= id 100)]
name)
;; => ("Albert Einstein")
(reduce (fn [v {:keys [id name]}]
(if (= id 100) (conj v name) v))
[]
authors)
;; => ["Albert Einstein"]
I prefer for (with :when) since it's shortest and in my eyes most clear. reduce I find best when you want to build a specific type of collection, this case a vector.
Filter will filter the list of maps. But the result is still a sequence of maps. You can map or reduce the result to get the list of authors.
(def authors [{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 100, :name "Richard Fynmann", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}])
(defn check-by-id [author id] (= id (:id author)))
(defn filter-ids [id col] (filter #(check-by-id % id) col))
(filter-ids 100 authors)
;; ↪ ({:id 100, :name "Albert Einstein", :interest "Physics"}
;; {:id 100, :name "Richard Fynmann", :interest "Physics"})
(map :name (filter-ids 100 authors))
;; ↪ ("Albert Einstein" "Richard Fynmann")
You can also use group-by for this task:
(def list-of-maps
[{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}])
(map :name (get (group-by :id list-of-maps) 100))
;; => ("Albert Einstein")

Spec: partially overriding generators in a map spec

Assuming I have already defined a spec from which I'd like to generate test data:
(s/def :customer/id uuid?)
(s/def :customer/given-name string?)
(s/def :customer/surname string?)
(s/def :customer/age pos?)
(s/def ::customer
(s/keys
:req-un [:customer/id
:customer/given-name
:customer/surname
:customer/age]))
In generating test data, I'd like to override how ids are generated in order to ensure they're from a smaller pool to encourage collisions:
(defn customer-generator
[id-count]
(gen/let [id-pool (gen/not-empty (gen/vector (s/gen :customer/id) id-count))]
(assoc (s/gen ::customer) :id (gen/element id-pool))))
Is there a way I can simplify this by overriding the :customer/id generator in my test code and then just using (s/gen ::customer)? So, something like the following:
(with-generators [:customer/id (gen/not-empty (gen/vector (s/gen :customer/id) id-count)))]
(s/gen ::customer))
Officially, you can override generators for specs by passing an overrides map to s/gen (See the docstring for more details):
(s/def :customer/id uuid?)
(s/def :customer/given-name string?)
(s/def :customer/surname string?)
(s/def :customer/age nat-int?)
(s/def ::customer
(s/keys
:req-un [:customer/id
:customer/given-name
:customer/surname
:customer/age]))
(def fixed-customer-id (java.util.UUID/randomUUID))
fixed-customer-id
;=> #uuid "c73ff5ea-8702-4066-a31d-bc4cc7015811"
(gen/generate (s/gen ::customer {:customer/id #(s/gen #{fixed-customer-id})}))
;=> {:id #uuid "c73ff5ea-8702-4066-a31d-bc4cc7015811",
; :given-name "1042IKQhd",
; :surname "Uw0AzJzj",
; :age 104}
Alternatively, there is a library for such stuff named genman, which I developed before :)
Using it, you can also write as:
(require '[genman.core :as genman :refer [defgenerator]])
(def fixed-customer-id (java.util.UUID/randomUUID))
(genman/with-gen-group :test
(defgenerator :customer/id
(s/gen #{fixed-customer-id})))
(genman/with-gen-group :test
(gen/generate (genman/gen ::customer)))
Clojure spec uses test.check internally to generate sample values. Here is how test.check can be overridden. Whenever trying to write unit tests with a "fake" function, with-redefs is your friend:
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require
[clojure.test.check.generators :as gen]
))
(def id-gen gen/uuid)
(dotest
(newline)
(spyx-pretty (take 3 (gen/sample-seq id-gen)))
(newline)
(with-redefs [id-gen (gen/choose 1 5)]
(spyx-pretty (take 33 (gen/sample-seq id-gen))))
(newline)
)
with result:
-----------------------------------
Clojure 1.10.3 Java 15.0.2
-----------------------------------
Testing tst.demo.core
(take 3 (gen/sample-seq id-gen)) =>
[#uuid "cbfea340-1346-429f-ba68-181e657acba5"
#uuid "7c119cf7-0842-4dd0-a23d-f95b6a68f808"
#uuid "ca35cb86-1385-46ad-8fc2-e05cf7a1220a"]
(take 33 (gen/sample-seq id-gen)) =>
[5 4 3 3 2 2 3 1 2 1 4 1 2 2 4 3 5 2 3 5 3 2 3 2 3 5 5 5 5 1 3 2 2]
Example created
using my favorite template project.
Update
Unfortunately, the above technique does not work for Clojure Spec since (s/def ...) uses a global registery of Spec definitions, and is therefore immune to with-redefs. However, we can overcome this definition by simply redefining the desired spec in the unit test namespace like:
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require
[clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen]
))
(s/def :app/id (s/int-in 9 99))
(s/def :app/name string?)
(s/def :app/cust (s/keys :req-un [:app/id :app/name]))
(dotest
(newline)
(spyx-pretty (gen/sample (s/gen :app/cust)))
(newline)
(s/def :app/id (s/int-in 2 5)) ; overwrite the definition of :app/id for testing
(spyx-pretty (gen/sample (s/gen :app/cust)))
(newline))
with result
-----------------------------------
Clojure 1.10.3 Java 15.0.2
-----------------------------------
Testing tst.demo.core
(gen/sample (s/gen :app/cust)) =>
[{:id 10, :name ""}
{:id 9, :name "n"}
{:id 10, :name "fh"}
{:id 9, :name "aI"}
{:id 11, :name "8v5F"}
{:id 10, :name ""}
{:id 10, :name "7"}
{:id 10, :name "3m6Wi"}
{:id 13, :name "OG2Qzfqe"}
{:id 10, :name ""}]
(gen/sample (s/gen :app/cust)) =>
[{:id 3, :name ""}
{:id 3, :name ""}
{:id 2, :name "5e"}
{:id 3, :name ""}
{:id 2, :name "y01C"}
{:id 3, :name "l2"}
{:id 3, :name "c"}
{:id 3, :name "pF"}
{:id 4, :name "0yrxyJ7l"}
{:id 4, :name "40"}]
So, it's a little ugly, but the redefinition of :app/id does the trick, and it only takes effect during unit test runs, leaving the main application unaffected.
user> (def ^:dynamic *idgen* (s/gen uuid?))
#'user/*idgen*
user> (s/def :customer/id (s/with-gen uuid? (fn [] ##'*idgen*)))
:customer/id
user> (s/def :customer/age pos-int?)
:customer/age
user> (s/def ::customer (s/keys :req-un [:customer/id :customer/age]))
:user/customer
user> (gen/sample (s/gen ::customer))
({:id #uuid "d18896f1-6199-42bf-9be3-3d0652583902", :age 1}
{:id #uuid "b6209798-4ffa-4e20-9a76-b3a799a31ec6", :age 2}
{:id #uuid "6f9c6400-8d79-417c-bc62-6b4557f7d162", :age 1}
{:id #uuid "47b71396-1b5f-4cf4-bd80-edf4792300c8", :age 2}
{:id #uuid "808692b9-0698-4fb8-a0c5-3918e42e8f37", :age 2}
{:id #uuid "ba663f0a-7c99-4967-a2df-3ec6cb04f514", :age 1}
{:id #uuid "8521b611-c38c-4ea9-ae84-35c8a2d2ff2f", :age 4}
{:id #uuid "c559d48d-4c50-438f-846c-780cdcdf39d5", :age 3}
{:id #uuid "03c2c114-03a0-4709-b9dc-6d326a17b69d", :age 40}
{:id #uuid "14715a50-81c5-48e4-bffe-e194631bb64b", :age 4})
user> (binding [*idgen* (let [idpool (gen/sample (s/gen :customer/id) 5)] (gen/elements idpool))] (gen/sample (s/gen ::customer)))
({:id #uuid "3e64131d-e7ad-4450-993d-fa651339df1c", :age 2}
{:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 1}
{:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 1}
{:id #uuid "3e64131d-e7ad-4450-993d-fa651339df1c", :age 1}
{:id #uuid "1a2eafed-8242-4229-b432-99edb361569d", :age 3}
{:id #uuid "1a2eafed-8242-4229-b432-99edb361569d", :age 1}
{:id #uuid "05bd521a-26f9-46e0-8b26-f798e0bf0452", :age 3}
{:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 19}
{:id #uuid "31b80714-7ae0-40a0-b932-f7b5f078f2ad", :age 2}
{:id #uuid "05bd521a-26f9-46e0-8b26-f798e0bf0452", :age 5})
user>
A little clumsier than what you wanted, but maybe this is adequate.
You are probably better off using binding rather than with-redefs since binding modifies thread-local bindings, whereas with-redefs changes the root binding.
Since this is for generating bad test data, I'd consider avoiding the use of dynamic vars and binding altogether and just use a different spec that is only local to the test env.

Using clojure.spec for a map

I have the following map:
(def gigs {:gig-01 {:id :gig-01
:title "Macaron"
:artist "Baher Khairy"
:desc "Sweet meringue-based rhythms with smooth and sweet injections of soul"
:img "https://res.cloudinary.com/schae/image/upload/f_auto,q_auto/v1519552695/giggin/baher-khairy-97645.jpg"
:price 1000
:sold-out false}
:gig-02 {:id :gig-02
:title "Stairs"
:artist "Brentr De Ranter"
:desc "Stairs to the highets peaks of music."
:img "https://res.cloudinary.com/schae/image/upload/f_auto,q_auto/v1519552695/giggin/brent-de-ranter-426248.jpg"
:price 2000
:sold-out false}})
I'd like to create a spec for it, but I'm not sure how to define the key e.g. ":gig-01" any pointers?
You could try:
(s/def ::gig-id
(s/and keyword?
(fn [x] (->> x name (re-matches #"gig-\d+")))))

Idiomatic way to select a map in vector by a key

Suppose I have this vector of maps:
[{:title "Title1" :id 18347125}
{:title "Title2" :id 18347123}
{:title "Title3" :id 18341121}]
And I wish to select the map with :id 18347125, how would I do this?
I've tried
(for [map maps
:when (= (:id map) id)]
map)
This feels a bit ugly and returns a sequence of length one, and I want to return just the map.
IMHO, there are several ways to solve your problem, and the definitely idiomatic way is in the realm of taste. This is my solution where I simply translated "to select maps whose :id is 1834715" into Clojure.
user> (def xs [{:title "Title1" :id 18347125}
{:title "Title2" :id 18347123}
{:title "Title3" :id 18341121}])
#'user/xs
user> (filter (comp #{18347125} :id) xs)
({:title "Title1", :id 18347125})
The :id keyword is a function that looks up itself in a collection passed to it. The set #{18347125} is also a function that tests if a value passed to it equals 18347125. Using a Clojure set as a predicate function allows for a succinct idiom.
I'm not sure if it's the simplest way to write it, but I think this is more clear about your intentions:
(->> maps
(filter #(= (:id %) id))
first)
This doesn't do what you asked for exactly, but might be useful nonetheless:
user=> (group-by :id [{:title "Title1" :id 18347125}
{:title "Title2" :id 18347123}
{:title "Title3" :id 18341121}])
{18347125 [{:title "Title1" :id 18347125}]
18347123 [{:title "Title2" :id 18347123}]
18341121 [{:title "Title3" :id 18341121}]}
Now you can simply look the map up by id. Read more about group-by on clojuredocs, its a very useful function.
Note that it puts the maps inside vectors. This is because group-by is designed to handle grouping (ie multiple items with the same key):
user=> (group-by :id [{:title "Title1" :id 123}
{:title "Title2" :id 123}
{:title "Title3" :id 18341121}])
{123 [{:title "Title1" :id 123} {:title "Title2" :id 123}]
18341121 [{:title "Title3" :id 18341121}]}
If you need to query not just once, but multiple times for maps with specific IDs, I would suggest to make your data types match your use case, i.e. to change the vector into a map:
(def maps-by-id (zipmap (map :id maps) maps))
So now your IDs are the keys in this new map of maps:
user=> (maps-by-id 18347125)
{:title "Title1", :id 18347125}

How best to update this tree?

I've got the following tree:
{:start_date "2014-12-07"
:data {
:people [
{:id 1
:projects [{:id 1} {:id 2}]}
{:id 2
:projects [{:id 1} {:id 3}]}
]
}
}
I want to update the people and projects subtrees by adding a :name key-value pair.
Assuming I have these maps to perform the lookup:
(def people {1 "Susan" 2 "John")
(def projects {1 "Foo" 2 "Bar" 3 "Qux")
How could I update the original tree so that I end up with the following?
{:start_date "2014-12-07"
:data {
:people [
{:id 1
:name "Susan"
:projects [{:id 1 :name "Foo"} {:id 2 :name "Bar"}]}
{:id 2
:name "John"
:projects [{:id 1 :name "Foo"} {:id 3 :name "Qux"}]}
]
}
}
I've tried multiple combinations of assoc-in, update-in, get-in and map calls, but haven't been able to figure this out.
I have used letfn to break down the update into easier to understand units.
user> (def tree {:start_date "2014-12-07"
:data {:people [{:id 1
:projects [{:id 1} {:id 2}]}
{:id 2
:projects [{:id 1} {:id 3}]}]}})
#'user/tree
user> (def people {1 "Susan" 2 "John"})
#'user/people
user> (def projects {1 "Foo" 2 "Bar" 3 "Qux"})
#'user/projects
user>
(defn integrate-tree
[tree people projects]
;; letfn is like let, but it creates fn, and allows forward references
(letfn [(update-person [person]
;; -> is the "thread first" macro, the result of each expression
;; becomes the first arg to the next
(-> person
(assoc :name (people (:id person)))
(update-in [:projects] update-projects)))
(update-projects [all-projects]
(mapv
#(assoc % :name (projects (:id %)))
all-projects))]
(update-in tree [:data :people] #(mapv update-person %))))
#'user/integrate-tree
user> (pprint (integrate-tree tree people projects))
{:start_date "2014-12-07",
:data
{:people
[{:projects [{:name "Foo", :id 1} {:name "Bar", :id 2}],
:name "Susan",
:id 1}
{:projects [{:name "Foo", :id 1} {:name "Qux", :id 3}],
:name "John",
:id 2}]}}
nil
Not sure if entirely the best approach:
(defn update-names
[tree people projects]
(reduce
(fn [t [id name]]
(let [person-idx (ffirst (filter #(= (:id (second %)) id)
(map-indexed vector (:people (:data t)))))
temp (assoc-in t [:data :people person-idx :name] name)]
(reduce
(fn [t [id name]]
(let [project-idx (ffirst (filter #(= (:id (second %)) id)
(map-indexed vector (get-in t [:data :people person-idx :projects]))))]
(if project-idx
(assoc-in t [:data :people person-idx :projects project-idx :name] name)
t)))
temp
projects)))
tree
people))
Just call it with your parameters:
(clojure.pprint/pprint (update-names tree people projects))
{:start_date "2014-12-07",
:data
{:people
[{:projects [{:name "Foo", :id 1} {:name "Bar", :id 2}],
:name "Susan",
:id 1}
{:projects [{:name "Foo", :id 1} {:name "Qux", :id 3}],
:name "John",
:id 2}]}}
With nested reduces
Reduce over the people to update corresponding names
For each people, reduce over projects to update corresponding names
The noisesmith solution looks better since doesn't need to find person index or project index for each step.
Naturally you tried to assoc-in or update-in but the problem lies in your tree structure, since the key path to update John name is [:data :people 1 :name], so your assoc-in code would look like:
(assoc-in tree [:data :people 1 :name] "John")
But you need to find John's index in the people vector before you can update it, same things happens with projects inside.