Skip to content

Instantly share code, notes, and snippets.

@ustun
Created March 1, 2017 22:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ustun/648af2ed6053e2edd1611b8308e07ff8 to your computer and use it in GitHub Desktop.
Save ustun/648af2ed6053e2edd1611b8308e07ff8 to your computer and use it in GitHub Desktop.
Let's say we have a feed of questions and each question has an answer, and each answer has an id.
We want to find the answer with id=5 and update its like count.
Assume the data structure is nested, so
(def feed (atom {:questions [{:id 1 :answers [{:id 5, text: 'foo', :like-count 4}]}]})
If I know that the answer I'm targeting is at 0th position of 0th question, I can do:
(swap! feed update-in [:questions 0 :answers 0 :like-count] inc)
But I don't actually know that it is at 0th position. I only know the answer id. What do I do then?
If it were mutable, it would be easy, since my "answer view" would actually hold on to the answer map. So,
I could just do (swap! my-answer update :like-count inc)
But it is not. One could use cursors for that, but will I create new cursors for each answer?
So, imagine the following:
(swap! feed update-in [:questions ALL :answers #(= :id 5) :like-count] inc)
Here, update took a path where we checked an arbitary predicate.
I run into this all the time in UI. Of course, if the data was modelled "normalized" or as in Datomic, so that my structure was like the following, I could just easily find and swap the answer.
(def feed (atom
{:question-ids ["q1" "q2"]
:q1 {:text "Question 1" :answer-ids ["a1" "a5"]}
:a5 {:text "Answer 5" :like-count 5}}))
Here, I can directly find a5.
(swap! feed update-in [:a5 :like-count] inc)
But now, my views need to do the denormalization. So whenever I display question 1 and its answers, I need to do a "db" lookup.
@ustun
Copy link
Author

ustun commented Mar 1, 2017

@borkdude
Copy link

borkdude commented Mar 1, 2017

I usually solve this by structuring it like:

(def m {:questions {1 {:id 1 :answers {5 {:id 5 :likes 100}}}}})
(update-in m [:questions 1 :answers 5 :likes] inc)
;; => {:questions {1 {:id 1, :answers {5 {:id 5, :likes 101}}}}}

@borkdude
Copy link

borkdude commented Mar 1, 2017

The alternative is writing a nested transformation:

(update m :questions (map (fn [q] (if (= 1 (:id q)) (update q :answers (mapv (fn [a] ...) ...

which would read better if you divided this up into some functions.

@ustun
Copy link
Author

ustun commented Mar 1, 2017

Yes, both solutions are valid, but see, you are changing your data because Clojure makes it unfriendly.
That is a red flag for a language that is data-driven.

But agreed that such deep nesting could be remedied by helper methods. In a normal app, I probably would have a helper method to transform a question, that is,

(defn increase-like-count-for-answer [question answer-id]
(update question :answers (fn [answers] (vec (for [answer answers] (if (= (:id ....
and

(defn increase-like-count [answer] ...

But still, my opinion is that deep updates are problematic in FP without a navigator abstraction.

FP decomplects identity, data and methods, but makes identity (not necessarily mutable though, I mean in the general sense to get a hold of a deeply nested value) tracking harder. Navigators could be an answer for this.

@borkdude
Copy link

borkdude commented Mar 1, 2017

;; sorry for missing indentation, I typed this directly in a REPL
(def feeds {:questions [{:id 1, :answers [{:id 5, :text "foo", :like-count 4}]}]})
(defn update-when [coll id-key v update-key f & args] (map (fn [e] (if (= v (get e id-key)) (apply update e update-key f args) e)) coll))
(update feeds :questions update-when :id 1 :answers update-when :id 5 :like-count inc)
;;=> {:questions ({:id 1, :answers ({:id 5, :text "foo", :like-count 5})})}

I think the last line reads pretty nice. update-when could be more general by replacing equality with a more general predicate.

@ustun
Copy link
Author

ustun commented Mar 1, 2017

Yes, at the very least, we need something in core like update-when that takes a list of predicates.

For reference, here is the version with specter:

(transform [:questions ALL #(= (:id %) 1) :answers ALL #(= (:id %) 2) :like-count] inc feed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment