Fork me on GitHub
#beginners
<
2016-08-13
>
senya2201:08:59

what would be the syntax to destructure a map looking something like {:c #{:b :a}, :f #{:e :c :b :d :a} .....} incoming as a parameter to a function so that inside that function I could distinguish between the key and a value of each map entry?

val_waeselynck01:08:15

@senya22: you could use something like (for [[k v] my-map] ...)

senya2202:08:55

@val_waeselynck: hm, and what if I want to use my-map as a second arg of a reducing function which already processes each individual entry of a map?

plexus07:08:43

@senya22: can you explain a bit more what you're after? maybe reduce-kv would help?

senya2213:08:00

@plexus: just wanted to walk over the elements in a map and compare its values to a static set and if its equal - to collect the key into the resulting collection. That comparison might need to be extended further to check not only for equality but for a subset as well...

plexus13:08:26

right... couple ways you could do that

plexus13:08:33

(let [m {:x 1 :y 2 :z 3}
      vset #{2 3}]
  (map first (filter (comp vset last) m)))

(let [m {:x 1 :y 2 :z 3}
      vset #{2 3}]
  (reduce-kv (fn [coll k v]
               (if (vset v) 
                 (conj coll v)
                 coll)) [] m))

plexus13:08:34

something like this?

senya2213:08:25

the first thing seems to be returning the keys

plexus13:08:50

you want the valus instead?

plexus13:08:33

if you have filter-kv availble (e.g. from Medley or Encore), you can do something like this, which I find reads a bit better

plexus13:08:40

(let [m {:x 1 :y 2 :z 3}
      vset #{2 3}]
  (keys (filter-kv (fn [k v] (vset v)) m)))

senya2213:08:50

no, no, I actually want the keys - I check the values if it's equal or a subset and accumulate the key

plexus13:08:04

yeah so instead of using a set literal you can define vset to be whatever test you want to perform on the values

senya2213:08:07

you mean in the

(let [m {:x 1 :y 2 :z 3}

     vset #{2 3}]
 (keys (filter-kv (fn [k v] (vset v)) m)))
?

senya2213:08:27

could you explain how the

(map first (filter (comp vset last) m)))
expression works please?

senya2213:08:43

it seems to be treating a map as a set of its entries.... or am I mistaken?

plexus13:08:24

sure, comp means compose, it takes two functions and returns a new function by chaining the two functions together, (comp vset last) is the same as #(last (vset %)), which is a shorthand for (fn [x] (last (vset x)))

plexus13:08:12

vset is a set (`#{2 3}`), if you treat a set like a function, then it tests if whatever you give it is in the set

plexus13:08:40

(#{1 2 3} 2) ;;=> true
(#{1 2 3} 4) ;;=> false

plexus13:08:07

if you use a function like filter, map, reduce over a map as we do here, then it treats the map as a sequence of "map entries", which you can think of as two-element vectors

plexus13:08:53

so (filter (comp vset last) m) with m being {:x 1 :y 2 :z 3} is the same as (filter #(vset (last %)) [[:x 1] [:y 2] [:z 3]])

plexus13:08:34

so the inner function first receives [:x 1], then [:x 2], then [:x 3]

plexus13:08:07

(last [:x 1]) ;;=> 1
(vset (last [:x 1])) ;;=> false

(last [:y 2]) ;;=> 2
(vset (last [:y 2])) ;;=> true

plexus13:08:21

the input of filter is a sequence of "map entries", so the output is also a sequence of map entries, just fewer

plexus13:08:46

here the output of filter is '([:y 2] [:z 3])

plexus13:08:17

for each of these we only want the first element (the key), hence (map first ,,,)

plexus14:08:22

does that make sense?

senya2214:08:34

yes, it certainly does 😉

senya2214:08:34

so the map is already treated as a sequence of its entries by default - there's no need to destructure it

senya2214:08:05

very interesting

senya2214:08:12

@plexus: that's great, thank you so much! Let me now try to incorporate all this knowledge into my task

senya2218:08:34

what would be the simplest way to flatten a map like this {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}} into the unique set of its values: #{:c :m :f}?

senya2218:08:39

Looks like an extra reduce needs to be added to something like this:

(reduce (fn[ flattened [key val]]
          (conj flattened val))
        #{}
        {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}})
=> #{#{:m :f} #{:c :f} #{:f}}

senya2218:08:01

But maybe there's a simpler way?

porglezomp19:08:49

clojure.set/union instead of conj should do it?

senya2219:08:13

yep, union seems to work

senya2219:08:00

why is it that the accumulator's type doesn' t really matter, whether I pass in a [], #{} or {}' - the results is always a set?

plexus19:08:24

can you show the code you have with union?

senya2219:08:59

(reduce (fn[ flattened [key val]]

          (clojure.set/union flattened val))
        {}
        {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}})
=> #{:m :c :f}

plexus19:08:30

actually in this case it's kind of a coincidence... that is to say, the set functions like union expect their inputs to be sets, but they don't check their inputs. In this case you're getting a set back out even though you're passing it a map and a set, but that behavior is not defined, you could have gotten a map back, or an error

senya2219:08:19

ah, ok - I'll leave it as set then

plexus19:08:53

(reduce (fn[ flattened [key val]]
         (clojure.set/union flattened val))
       {:x 1 :y 2 :z 3}
       {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}})
;;=> BOOM, this blows up

(reduce (fn[ flattened [key val]]
         (clojure.set/union flattened val))
       [1 2 3]
       {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}})
;; and now it's a vector ¯\_(ツ)_/¯
;;=> [1 2 3 :m :f :f :c :f :m :f :c :f]

plexus19:08:09

this is called "garbage in - garbage out", you're expected to use these functions as they are intended to be used, if you don't all bets are off. This is (in this case) considered preferable to incurring the extra overhead of checking the input types

plexus19:08:10

presumably once Clojure 1.9 is out there will be specs for these functions, and so you will be able to turn on instrumentation during development so you do get that type checking

senya2219:08:21

weird, this still returns a set:

(reduce (fn[ flattened [key val]]

          (clojure.set/union flattened val))
        [1]
        {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}})
=> #{1 :m :c :f}

plexus19:08:50

yeah 🙂 I had to look at how union is implemented to come up with those examples 😉

plexus19:08:09

(defn union
  "Return a set that is the union of the input sets"
  {:added "1.0"}
  ([] #{})
  ([s1] s1)
  ([s1 s2]
     (if (< (count s1) (count s2))
       (reduce conj s2 s1)
       (reduce conj s1 s2)))
  ([s1 s2 & sets]
     (let [bubbled-sets (bubble-max-key count (conj sets s2 s1))]
       (reduce into (first bubbled-sets) (rest bubbled-sets)))))

plexus19:08:29

you're only calling it with two arguments, so you only care about this middle part

plexus19:08:39

(if (< (count s1) (count s2))
       (reduce conj s2 s1)
       (reduce conj s1 s2))

plexus19:08:00

in other words, it's taking whichever collecting is bigger, and conj'ing the elements of the other one onto it

senya2219:08:40

so you are saying it's better to make an accumulator a set to start with, right?

plexus20:08:04

yes! since you're passing it to union it really should be a set

plexus20:08:23

it also conceptually makes sense for what you're doing

senya2220:08:29

ok, makes sense

senya2220:08:29

I tried to implement this via some 2 functions but that didn't lead me far:

(defn flatten-dpdnts[dpdts-map]
  (reduce apply-flatten #{} dpdts-map))

(defn apply-flatten [flattened dpdnts]
  (let [[key val] dpdnts]
    (reduce #(conj flattened %1) flattened val))
  )

senya2220:08:36

doesn't quite work as expected

plexus20:08:13

that reduce looks a bit funny, probably should be (reduce #(conj %1 %2) flattened val)

plexus20:08:20

or just (conj flattened val)... I think, hard to say without knowing the shape of your data

plexus20:08:39

dpdts-map is something like {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}}?

senya2220:08:31

yes, it's what I passed as an arg in that function with union we just discussed

plexus20:08:56

(defn apply-flatten [flattened dpdnts]
 (let [[key val] dpdnts]
   (conj flattened val)))

(defn flatten-dpdnts [dpdts-map]
 (reduce apply-flatten #{} dpdts-map))

(flatten-dpdnts {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}})

;;=> #{#{:m :f} #{:c :f} #{:f}}

plexus20:08:26

your reducer was trying to do too much 🙂

plexus20:08:18

or I guess you just want one set back, then you can again replace the conj with union

plexus20:08:57

(sorry, I'm just rambling at this point, always fun to help solve these little puzzles)

senya2220:08:29

🙂 ah, I had it on this stage once, but then I wanted to do another iteration to have a single set

senya2220:08:00

that's when I tried introducing the second reduce

plexus20:08:32

yeah you were really close actually, notice how your second reduce is very similar to the one in union

plexus20:08:11

(defn apply-flatten [flattened dpdnts]
  (let [[key val] dpdnts]
    (reduce conj flattened val)))

(defn flatten-dpdnts [dpdts-map]
  (reduce apply-flatten #{} dpdts-map))

(flatten-dpdnts {:e #{:m :f}, :c #{:f}, :b #{:c :f}, :d #{:m :f}, :a #{:c :f}})
;;=> #{:m :c :f}

plexus20:08:32

just changed conj to reduce conj and that's it

senya2220:08:13

he-he, it would have taken me another day to figure out 🙂

plexus20:08:25

or into would also have work, as in (into flattened val)

senya2220:08:21

into seems more readable

madstap20:08:46

Solving these puzzles is fun, here's my take on it. It relies on clojure.set/union being variadic, no need for reduce.

(defn flatten-dpdnts [dpdnts-map]
    (apply set/union (vals dpdnts-map)))