Idiomatic way to do swap! or update-in using Datomic - clojure

If I'm using a Clojure atom to keep count of something (votes for example), I can do this:
(def votes (atom {}))
(defn vote! [candidate]
(swap! votes update-in [candidate] (fnil inc 0)))
(vote! "Abraham Lincoln")
(vote! "Abraham Lincoln")
(vote! "Winston Churchill")
votes ;=> {"Abraham Lincoln" 2, "Winston Churchill" 2}
Here, update-in neatly transforms the value at the given key, without having to look it up first.
How can I accomplish the same in Datomic? I could do something like this...
(defn vote! [db candidate]
(let [[e v] (first (q '[:find ?e ?v
:in $ ?candidate
:where [[?e :name ?candidate]
[?e :votes ?v]] db candidate)
(transact! conn [{:db/id e :votes (inc v)}]))
But it seems a bit cumbersome, having to run the query, return the value and then transact with the new value. Is there a more idiomatic way of doing this (like a swap! or update-in)?

To me it seems most idiomatic to record the fact of the vote, then make a count query when you want to know the total. Depending on the needs of your application, this approach supports things like checking for double votes, vote change/retraction, counts in the last 24 hours, ...

Related

Clojure/FP: apply functions to each argument to an operator

Let's say I have several vectors
(def coll-a [{:name "foo"} ...])
(def coll-b [{:name "foo"} ...])
(def coll-c [{:name "foo"} ...])
and that I would like to see if the names of the first elements are equal.
I could
(= (:name (first coll-a)) (:name (first coll-b)) (:name (first coll-c)))
but this quickly gets tiring and overly verbose as more functions are composed. (Maybe I want to compare the last letter of the first element's name?)
To directly express the essence of the computation it seems intuitive to
(apply = (map (comp :name first) [coll-a coll-b coll-c]))
but it leaves me wondering if there's a higher level abstraction for this sort of thing.
I often find myself comparing / otherwise operating on things which are to be computed via a single composition applied to multiple elements, but the map syntax looks a little off to me.
If I were to home brew some sort of operator, I would want syntax like
(-op- (= :name first) coll-a coll-b coll-c)
because the majority of the computation is expressed in (= :name first).
I'd like an abstraction to apply to both the operator & the functions applied to each argument. That is, it should be just as easy to sum as compare.
(def coll-a [{:name "foo" :age 43}])
(def coll-b [{:name "foo" :age 35}])
(def coll-c [{:name "foo" :age 28}])
(-op- (+ :age first) coll-a coll-b coll-c)
; => 106
(-op- (= :name first) coll-a coll-b coll-c)
; => true
Something like
(defmacro -op-
[[op & to-comp] & args]
(let [args' (map (fn [a] `((comp ~#to-comp) ~a)) args)]
`(~op ~#args')))
Is there an idiomatic way to do this in clojure, some standard library function I could be using?
Is there a name for this type of expression?
For your addition example, I often use transduce:
(transduce
(map (comp :age first))
+
[coll-a coll-b coll-c])
Your equality use case is trickier, but you could create a custom reducing function to maintain a similar pattern. Here's one such function:
(defn all? [f]
(let [prev (volatile! ::no-value)]
(fn
([] true)
([result] result)
([result item]
(if (or (= ::no-value #prev)
(f #prev item))
(do
(vreset! prev item)
true)
(reduced false))))))
Then use it as
(transduce
(map (comp :name first))
(all? =)
[coll-a coll-b coll-c])
The semantics are fairly similar to your -op- macro, while being both more idiomatic Clojure and more extensible. Other Clojure developers will immediately understand your usage of transduce. They may have to investigate the custom reducing function, but such functions are common enough in Clojure that readers can see how it fits an existing pattern. Also, it should be fairly transparent how to create new reducing functions for use cases where a simple map-and-apply wouldn't work. The transducing function can also be composed with other transformations such as filter and mapcat, for cases when you have a more complex initial data structure.
You may be looking for the every? function, but I would enhance clarity by breaking it down and naming the sub-elements:
(let [colls [coll-a coll-b coll-c]
first-name (fn [coll] (:name (first coll)))
names (map first-name colls)
tgt-name (first-name coll-a)
all-names-equal (every? #(= tgt-name %) names)]
all-names-equal => true
I would avoid the DSL, as there is no need and it makes it much harder for others to read (since they don't know the DSL). Keep it simple:
(let [colls [coll-a coll-b coll-c]
vals (map #(:age (first %)) colls)
result (apply + vals)]
result => 106
I don't think you need a macro, you just need to parameterize your op function and compare functions. To me, you are pretty close with your (apply = (map (comp :name first) [coll-a coll-b coll-c])) version.
Here is one way you could make it more generic:
(defn compare-in [op to-compare & args]
(apply op (map #(get-in % to-compare) args)))
(compare-in + [0 :age] coll-a coll-b coll-c)
(compare-in = [0 :name] coll-a coll-b coll-c)
;; compares last element of "foo"
(compare-in = [0 :name 2] coll-a coll-b coll-c)
I actually did not know you can use get on strings, but in the third case you can see we compare the last element of each foo.
This approach doesn't allow the to-compare arguments to be arbitrary functions, but it seems like your use case mainly deals with digging out what elements you want to compare, and then applying an arbitrary function to those values.
I'm not sure this approach is better than the transducer version supplied above (certainly not as efficient), but I think it provides a simpler alternative when that efficiency is not needed.
I would split this process into three stages:
transform items in collections into the data in collections you want to operate
on - (map :name coll);
Operate on transformed items in collections, returning collection of results - (map = transf-coll-a transf-coll-b transf-coll-c)
Finally, selecting which result in resulting collection to return - (first calculated-coll)
When playing with collections, I try to put more than one item into collection:
(def coll-a [{:name "foo" :age 43} {:name "bar" :age 45}])
(def coll-b [{:name "foo" :age 35} {:name "bar" :age 37}])
(def coll-c [{:name "foo" :age 28} {:name "bra" :age 30}])
For example, matching items by second char in :name and returning result for items in second place:
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp #(nth % 1) :name)
op =
fetch second]
(fetch (apply map op (map #(map transf-fn %) colls))))
;; => false
In transducers world you can use sequence function which also works on multiple collections:
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp (map :name) (map #(nth % 1)))
op =
fetch second]
(fetch (apply sequence (map op) (map #(sequence transf-fn %) colls))))
Calculate sum of ages (for all items at the same level):
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp (map :age))
op +
fetch identity]
(fetch (apply sequence (map op) (map #(sequence transf-fn %) colls))))
;; => (106 112)

Is (def m (update-in m ks f & args)) a good practice?

I'm new to the world of clojure and I have a doubt.
I have a nested map such as
(def accounts (hash-map :XYZ (hash-map :balance (hash-map 171000 0 :171018 500 :171025 200)
:statement (hash-map :171018 [{:desc "purchase" :amount 200}
{:desc "deposit" :amount 700}]
:171025 [{:desc "purchase" :amount 300}]))
And I want to update the statements, so I wrote a simple function:
(defn add-statement
[account date desc amount]
(def accounts (update-in accounts [account :statement date] conj {:desc desc :amount amount}))
But I have a feeling that I'm doing that the wrong way...
You would need to change accounts to be mutable if you want to update them. The usual way to do this is by making accounts an atom. Then your function might look like this:
(defn add-statement! [account date desc amount]
(swap! accounts update-in [account :statement date]
(fn [line-items]
(conj (or line-items []) {:desc desc :amount amount}))))
This will add a statement line-item at a new or an existing date. The update-in is the same as yours, except that a line-item for a new date will be put into a vector rather than a list. (conj keeps the type, but it has to know the type: (conj nil :a) gives (:a)).
To turn accounts into an atom:
(def accounts (atom (hash-map ...)))
I notice your balances are not correct anyway. But if you are updating them be sure to do so in the same swap! function.
To answer your question, "Is (def m (update-in m ...)) a good practice?". Definitely not inside a defn. If you are thinking to put a def inside a defn use a let instead. Outside of a defn it would be fine to have a def that updates another def that has a different name, so (def m2 (update-in m1 ...)).

Mapping a list of datomic ids to entity maps

When I query for a list of Datomic entities, e.g like in the example below:
'[:find ?e
:where
[?e :category/name]]
Usually, I'd like to create a list of maps that represent the full entities, i.e
#{[1234] [2223]} => [{:category/name "x" :db/id 1234}, {:category/name "y" :db/id 2223}]
Here is my approach at the moment, in the form of a helper function.
(defn- db-ids->entity-maps
"Takes a list of datomic entity ids retrieves and returns
a list of hydrated entities in the form of a list of maps."
[db-conn db-ids]
(->>
db-ids
seq
flatten
(map #(->>
%
;; id -> lazy entity map
(d/entity (d/db db-conn))
;; realize all values, except for db/id
d/touch
(into {:db/id %})))))
Is there a better way?
With the pull api, this is pretty easy now.
'[:find [(pull ?e [*]) ...]
:in $ [[?e] ...]
:where [?e]]
I used to take this approach to save queries to the DB, the code is probably less reusable but it depends on what is more critical in your current scenario. I haven't a Datomic instance configured as I am not working with it right now so it may contain syntax error but I hope you get the idea.
(def query-result '[:find ?cat-name ?id
:where
[?cat-name :category/name
[?id :db/id]])
=>
#{["x" 1234] ["x" 2223]}
(defn- describe-values
"Adds proper keys to the given values."
[keys-vec query-result]
(vec (map #(zipmap keys-vec %) query-result))
(describe-values [:category/name :db/id] query-result)
=>
[{:db/id 2223, :category/name "x"} {:db/id 1234, :category/name "x"}]

StackOverflow when filtering datoms

I'm trying to filter through a database, based on the keyword patter match.
To do this, I wrote ->
(defn find-users
[db keyword]
(if (>= (count keyword) 3)
(let [login-pattern (login-pattern keyword)]
(->> (d/datoms db :aevt :user/name)
(filter #(re-matches login-pattern (:v %)))
(map #(d/entity db (:e %)))))
[]))
But, I'm getting a StackOverflow error.
I think it's because of (map #(d/entity db (:e %)))
When I plainly do (map :e), the function works.
I'm a bit confused as to why the Stackoverflow would happen though, the queries I'm performing with map :v are returning only a few entities.
What's happening here?
I'm a little curious as to why not just use query? You can use predicate expression clauses for the same purpose that you've built the filter/map scenario above. See "Expression Clauses" at: http://docs.datomic.com/query.html
(defn find-users
[db keyword]
(if (>= (count keyword) 3)
(map #(d/entity db (first %))
(d/q '[:find ?e
:in $ ?login-pattern
:where
[?e :user/name ?name]
[(re-matches ?login-pattern ?name)]]
db
(login-pattern keyword)))
[]))
The query engine is likely to handle the size of intermediate results better than raw sequence manipulations.

Problems with "Revisiting the past" section of Datomic Tutorial

I'm having problems with the datomic tutorial at the "Revisiting the past" section http://datomic.com/company/resources/tutorial.html
For the two queries below:
query = "[:find ?c :where [?c :community/name]]";
db_asOf_schema = conn.db().asOf(schema_tx_date);
System.out.println(Peer.q(query, db_asOf_schema).size()); // 0
db_since_data = conn.db().since(data_tx_date);
System.out.println(Peer.q(query, db_since_data).size()); // 0
I have tried these commands in clojure, but cannot get them working as described in the tutorial:
(since (db conn) (java.util.Date.) )
;; It should return 0 but returns the whole database instead
(def ts (q '[:find ?when :where [?tx :db/txInstant ?when]] (db conn)))
(count (since (db conn) (ffirst (reverse (sort ts))))))
;; returns 13, but should return 0
(count (as-of (db conn) (ffirst (sort ts)))))
;; returns 13, but should return 0
I'm not too sure is this is the right behaviour, is there anything I'm doing wrong?
If you are working through the Seattle tutorial in Clojure, probably the most important single thing to know is that working Clojure code is included in the Datomic distribution. The filename is samples/seattle/getting-started.clj, and you can simply follow along at the REPL.
Two observations on the Clojure code in your question:
The since function is documented to return a database value, not a number, so the behavior you are seeing is as expected. In order to see what is in the database, you need to issue a query.
Databases do not have any documented semantics for the Clojure count function, so you should not call count on them. Again, if you want to see what is in the database, you need to issue a query, e.g.
;; Find all transaction times, sort them in reverse order
(def tx-instants (reverse (sort (q '[:find ?when :where [_ :db/txInstant ?when]]
(db conn)))))
;; pull out two most recent transactions, most recent loaded
;; seed data, second most recent loaded schema
(def data-tx-date (ffirst tx-instants))
(def schema-tx-date (first (second tx-instants)))
;; make query to find all communities
(def communities-query '[:find ?c :where [?c :community/name]])
;; find all communities as of schema transaction
(let [db-asof-schema (-> conn db (d/as-of schema-tx-date))]
(println (count (seq (q communities-query db-asof-schema)))))
;; find all communities as of seed data transaction
(let [db-asof-data (-> conn db (d/as-of data-tx-date))]
(println (count (seq (q communities-query db-asof-data)))))
;; find all communities since seed data transaction
(let [db-since-data (-> conn db (d/since data-tx-date))]
(println (count (seq (q communities-query db-since-data)))))
Hope this helps. There is also a Datomic google group if you have more questions.