Search code examples
clojure

Hash-map not displaying as hash-map


New to clojure. Trying to solve the following problem with a java background. I need to transform table to a hash-map that maps products to all the cities that sell the product. So the output should be.

{"Pencil": ("Oshawa" "Toronto")
 "Bread": ("Ottawa" "Oshawa" "Toronto")}


(def table [
        {:product "Pencil"
        :city "Toronto"
        :year "2010"
        :sales "2653.00"}
        {:product "Pencil"
        :city "Oshawa"
        :year "2010"
        :sales "525.00"}
        {:product "Bread"
        :city "Toronto"
        :year "2010"
        :sales "136,264.00"}
        {:product "Bread"
        :city "Oshawa"
        :year "nil"
        :sales "242,634.00"}
        {:product "Bread"
        :city "Ottawa"
        :year "2011"
        :sales "426,164.00"}])

This is what I have so far. I write this code into the repl.

(let [product-cities {}]
(for [row table]
  (if (= (contains? product-cities (keyword (row :product))) true)
    (println "YAMON") ;;To do after. Add city to product if statement is true
    (into product-cities {(keyword (row :product)) (str (row :city))}))))

However, the outcome is the following:

({:Pencil "Toronto"}
 {:Pencil "Oshawa"}
 {:Bread "Toronto"}
 {:Bread "Oshawa"}
 {:Bread "Ottawa"})

My if statement keeps returning false. I see that there are semi-circle brackets around the many hash-maps. I can't figure out why it's not returning one hashmap and why there are many hashmap? Thanks

EDIT:

QUESTION 2: Transform table to a hash-map that maps products to the city that has the highest sale. For example, the output should look like:

{"Pencil": "Toronto"
 "Bread": "Ottawa"}

I think a different strategy is needed than building up a value but here's what I'm thinking:

(reduce (fn [product-cities {:keys [product city sales]}]
      (update-in product-cities [product] (fnil conj []) {(keyword city) sales}))
    {}
    table)

This produces the following output:

{"Bread"
 [{:Toronto "136,264.00"}
  {:Oshawa "242,634.00"}
   {:Ottawa "426,164.00"}],
 "Pencil"
 [{:Toronto "2653.00"}
  {:Oshawa "525.00"}]}

I could then use the reduce function again but only add the city with max sales. I don't think this is the most efficient way.


Solution

  • You seem to have some misconceptions about how clojure works. Coming from java it can be hard to know how to do stuff, just because it's so different. This small problem serves nicely as an introduction to how to build up a value, and I'll try to explain each part of the solution.

    Immutability

    It's a common pattern in java to define a variable that will hold the final result, then loop through something while adding to that variable.

    That's what you're trying to do with your product-cities local. When you define a local with let in clojure it never changes, so to build up a value you need another pattern.

    Adding something to a map

    First let's take a look at how to "add something to a map". In clojure what you actually do is make a new map with the thing added. The old map doesn't change. We still sometimes phrase it as adding to a map, but that's just shorthand for "make a new map with the thing added".

    assoc takes a map, a key and a value and returns a new map with the value added at the key. If there's already a value there it will be overwritten. We want multiple things for each key, so it's not the right thing in this case.

    update is similar, but it takes a map, a key, a function and optionally arguments to that function. It will call the function with the value that's already at key as the first argument and (if present) the arguments supplied. The returned map will have the return value of the function as the new value at key. Some examples might make this clearer.

    ;; The function - is called with the old value of :foo and the argument supplied
    ;; (- 10 3)
    (update {:foo 10} :foo - 3) ;=> {:foo 7}
    

    If there's nothing already at key, the function will be called with nil as the first argument. That's what nil means, nothing.

    (update {} :foo + 5) ;=> Null pointer exception. Same as (+ nil 5)
    

    Null pointers are no good. There's a trick for avoiding them. fnil is a higher order function that takes a function and arguments. It returns a new function that will substitute a nil argument for the arguments supplied.

    ;; The nil here is substituted with 0, so no NPE
    ((fnil + 0) nil 5) ;=> 5
    
    ;; If the arg is not nil, the zero is not used.
    ((fnil + 0) 5 5) ;=> 10
    
    ;; So now we can update the value at :foo regardless of whether it's already there.
    (update {} :foo (fnil + 0) 5) ;=> {:foo 5}
    

    conj adds something to a collection. If that collection is a vector, it adds it at the end.

    (conj [] :foo) ;=> :foo
    
    (conj [:foo] :bar) ;=> [:foo :bar]
    

    To add things to the map we can combine these:

    (update {} "product" (fnil conj []) "city") ;=> {"product ["city"]"}
    
    (update {"product" ["city"]} "product" (fnil conj []) "another city")
    ;;=> {"product" ["city" "another city"]}
    

    Building up a value

    We need to do some looping somehow. A for in clojure is a list comprehension however, and not a for loop. It will return a sequence of things, so it's not the right thing to use when you want to build up a value.

    One way to do it is with loop.

    With a loop you define binding names paired with their initial value. The bindings in loop can be thought of as "the things that are going to change during the loop".

    One difference between loop and traditional loops is that to break out of the loop you simply don't do anything, and you need to specifically use recur to keep looping.

    ;; the bindings are pairs of names and initial values
    (loop [product-cities {} ; This is the accumulator that we're gonna build up.
           rows table] ; and these are the rows, we're gonna go through them one by one.
    
      ;; Here's the base case, if there are no rows left we return the accumulator.
      (if (empty? rows)
        product-cities
    
        ;; If there are rows left, we need to add to the accumulator.
        (let [row (first rows)
              city (:city row)
              product (:product row)
              new-accumulator (update product-cities product (fnil conj []) city)]
    
          ;; recur takes as many arguments as the pairs we defined
          ;;  and "jumps" back to the loop
          (recur new-accumulator (rest rows)))))
    ;;=> {"Pencil" ["Toronto" "Oshawa"], "Bread" ["Toronto" "Oshawa" "Ottawa"]}
    

    This can be made nicer with some destructuring.

    (loop [product-cities {}
           [{:keys [city product] :as row} & rows] table]
      (if (nil? row)
        product-cities
        (recur (update product-cities product (fnil conj []) city) rows)))
    ;;=> {"Pencil" ["Toronto" "Oshawa"], "Bread" ["Toronto" "Oshawa" "Ottawa"]}
    

    loop is not much used in normal clojure though. It's too general and usually you want something more specific. Like in this case, you want to build up a value while looping through every thing in a sequence of things. That's what reduce does.

    Reduce takes three arguments, a function, an initial value and a collection. The function, called a "reducing function" takes two arguments; the accumulated value so far and an item. It is called once for each item in the collection.

    So the final implementation becomes:

    (reduce (fn [product-cities {:keys [product city]}]
              (update product-cities product (fnil conj []) city))
            {}
            table)
    

    Edit:

    About your comment on the other answer. update was added in clojure 1.7.0 so you're presumable on an older version. You can use update-in instead (though you should consider upgrading). It's called in exactly the same way except the key is in a vector.

    (reduce (fn [product-cities {:keys [product city]}]
              (update-in product-cities [product] (fnil conj []) city))
            {}
            table)