Search code examples
clojurelispcommon-lispimmutability

Immutability in clojure when incrementing numbers inside my own data structure, going from common lisp mutability to clojure immutability


I've played around a bit with clojure before doing much more studying of lisp and I want to get back into clojure...

So I have this thing in common lisp:

(mapcar #'(lambda (x)
        (incf (third (car (member x count-mx
                      :test #'equal
                      :key #'(lambda (x) (list (car x) (cadr x))))))))
    transitions)

That for me in clojure translated to (not sure if this is good clojure):

(map (fn [x] (swap! (third (some #(if (= x (vec (first %) (second %))) %)
                 count-mx))
            inc))
     transitions)

where count-mx, if I used atoms to make the zeroes:

[[a a 0] [a b 0] [a c 0] [a d 0] [b a 0] [b b 0] [b c 0] [b d 0] [c a 0] [c b 0] [c c 0] [c d 0] [d a 0] [d b 0] [d c 0] [d d 0]]

and transitions:

[[a a] [a b] [b c] [c d] [d a] [a b] [b c]]

The objective being to increment [a a 0] to [a a 1] after mapping over transitions and seeing [a a]

Although the clojure line does work I cannot use it within a function or let because of immutability, how do I break out of my common lisp thinking? I quite like mapping and modifying values within something, but I'm not sure how to do this efficiently and/or correctly in clojure.


Solution

  • Edit (Improved answer)

    I didn't know: Maps accept also vectors as keys. In this case, we can directly take the pairs as keys and use update in combination with inc to update the values (by enhancing the value by 1). I use sorted-map because it looks nicer for presentation (sorted) - more performant would be to use (hash-map) or {} in the into expression.

    (def m (into (sorted-map)        ;; or: `{}` btw `(hash-map)`
                 (for [x '[a b c d]  ;; nesting for-loop
                       y '[a b c d]] ;; for sunccinct creation 
                   {[x y] 0})))      ;; of all combinations
    
    m 
    ;; => {[a a] 0, [a b] 0, [a c] 0, [a d] 0, 
    ;;     [b a] 0, [b b] 0, [b c] 0, [b d] 0, 
    ;;     [c a] 0, [c b] 0, [c c] 0, [c d] 0, 
    ;;     [d a] 0, [d b] 0, [d c] 0, [d d] 0}
    
    
    (def transitions '[[a a] [a b] [b c] [c d] 
                       [d a] [a b] [b c]])
    
    (def m' (reduce (fn [a-map a-transition] 
                      (update a-map a-transition inc))
                    m
                    transitions))
    
    m'
    ;; => {[a a] 1, [a b] 2, [a c] 0, [a d] 0, 
    ;;     [b a] 0, [b b] 0, [b c] 2, [b d] 0, 
    ;;     [c a] 0, [c b] 0, [c c] 0, [c d] 1, 
    ;;     [d a] 1, [d b] 0, [d c] 0, [d d] 0}
    
    

    (reduce (fn [mp key] (update mp key inc)) m '[a b c]) expands to: (update (update (update m 'a inc) 'b inc) 'c inc) therefore (in an accumulating manner) sequentially updating the values of each key given in transitions.

    We convert the sorted map into the nested vector form by:

    (def m'' (into [] (map (comp vec flatten) (seq m'))))
    
    m''
    ;; => [[a a 1] [a b 2] [a c 0] [a d 0] 
    ;;     [b a 0] [b b 0] [b c 2] [b d 0] 
    ;;     [c a 0] [c b 0] [c c 0] [c d 1] 
    ;;     [d a 1] [d b 0] [d c 0] [d d 0]]
    
    
    

    We can generalize them to functions:

    (defn update-map-by-transitions 
      "Update the map `m` successively by transitions"
      [m transitions & {:keys [func] :or {func inc}}]
      (reduce (fn [m' tr] (update m' tr func))
              m
              transitions))
    
    (defn map-to-vecs 
      "Transform transition map to flatten vectors"
      [m & {:keys [vec-func] :or {vec-func flatten}}] 
      (into [] (map (comp vec vec-func) (seq m))))
              
    (def m' (update-map-by-transitions m transitions))
    (def m'' (map-to-vecs m' :vec-func flatten))
    
    m''
    ;; [[a a 1] [a b 2] [a c 0] [a d 0] 
    ;;  [b a 0] [b b 0] [b c 2] [b d 0] 
    ;;  [c a 0] [c b 0] [c c 0] [c d 1] 
    ;;  [d a 1] [d b 0] [d c 0] [d d 0]]
    
    ;; or do:
    (def m''' (map-to-vecs m' :vec-func identity))
    m'''
    ;; [[[a a] 1] [[a b] 2] [[a c] 0] [[a d] 0] 
    ;;  [[b a] 0] [[b b] 0] [[b c] 2] [[b d] 0] 
    ;;  [[c a] 0] [[c b] 0] [[c c] 0] [[c d] 1] 
    ;;  [[d a] 1] [[d b] 0] [[d c] 0] [[d d] 0]]
    
    

    Original answer

    I am also coming from Common Lisp. By the way, you abbreviate it CL or "Lisp". But CLISP actually means one of the many implementations of CL (next to sbcl, abcl, ECL, etc.). So don't call CL clisp ... (to avoid others to be annoyed about it).

    I think using nested vectors for this task is tedious and - when scaling up later - less performant.

    Instead, prefer maps. They have the function update and in case of nested maps update-in to manipulate and "mutate" the values of a map which will prove very handy for this task.

    To take a vec [a b c d] and generate {a 0 b 0 c 0 d 0}, I would define:

    (defn mapper [vec default-val]
      (into {} (for [x vec] {x default-val})))
    
    (mapper '[a b] 0) ;; => {a 0 b 0}
    

    Now, we want two keys for storing a count value for them: Nest the maps:

    (def m (mapper '[a b c d] (mapper '[a b c d] 0)))
    
    m
    ;; =>{a {a 0, b 0, c 0, d 0}, 
    ;;    b {a 0, b 0, c 0, d 0}, 
    ;;    c {a 0, b 0, c 0, d 0}, 
    ;;    d {a 0, b 0, c 0, d 0}}
    
    

    We can anytime convert them back to your favorized nested vector form (I admit, it is better human readable):

    (defn nested-maps-to-vecs [nested-map]
      (vector (for [[k v] nested-map
            [ik iv] v]
        [k ik iv])))
    
    (nested-maps-to-vecs m)
    ;;=> [[a a 0] [a b 0] [a c 0] [a d 0] 
    ;;    [b a 0] [b b 0] [b c 0] [b d 0] 
    ;;    [c a 0] [c b 0] [c c 0] [c d 0] 
    ;;    [d a 0] [d b 0] [d c 0] [d d 0]]
    

    Now define transitions:

    (def transitions '[[a a] [a b] [b c] [c d] [d a] [a b] [b c]])
    

    Using inc function for update and the handy update-in we can "mutate" the values of the map:

    (def m' (reduce (fn [a-map a-transition] (update-in a-map a-transition inc)) m transitions))
    
    m'
    ;; => {a {a 1, b 2, c 0, d 0}, 
    ;;     b {a 0, b 0, c 2, d 0}, 
    ;;     c {a 0, b 0, c 0, d 1}, 
    ;;     d {a 1, b 0, c 0, d 0}}
    
    (nested-maps-to-vecs m')
    ;; => [[a a 1] [a b 2] [a c 0] [a d 0] 
    ;;     [b a 0] [b b 0] [b c 2] [b d 0] 
    ;;     [c a 0] [c b 0] [c c 0] [c d 1] 
    ;;     [d a 1] [d b 0] [d c 0] [d d 0]]
    
    

    Voila!

    (update-in a-map a-transition inc) gets e.g. [a b] and accesses in the nested map with the key sequence a and then b and takes the value which is in that "place" and applies inc to it and "stores" the result into that "place". Well, it doesn't store it actually, but it returns a new map with the updated value. This new map is the new a-map in the reduce function and taking the next a-transition this updated map will be further updated. So reduce is the trick to "accumulate" the updates by constant update and capture of the updated map while walking through the transitions sequence.

    Clojure has a very efficient way to "update" its immutable structures. It saves only the changes while referring to the unchanged rest. Therefore this "generation of a new updated map" by update sounds worse than it actually is: It is actually highly efficient performance- as well as memory footprint-wise. This special storing strategy cannot be pursued by Common Lisp and other Lisps or languages, because of mutability. Only pure functional languages with ensured immutability of data like Clojure or Haskell can use this kind of strategy for storing and updating their immutable data.

    Clojure has however also mutability in atoms which has to be specifically declared to be an atom. In this case, to use atoms as values in the map or the nested vectors would be not very the Clojure way.

    I am also still trying to wrap my head around this all.

    Have fun in your journey!