Reputation: 336
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.
Upvotes: 1
Views: 195
Reputation: 6509
You need to go through the table one by one and accumulate (conj
) the cities under the product:
(reduce
(fn [acc {:keys [product city]}]
(update acc product conj city))
{}
table)
;; => {"Pencil" ("Oshawa" "Toronto"), "Bread" ("Ottawa" "Oshawa" "Toronto")}
The use of the updating function (conj
in this case) can be a bit tricky, so here is an alternative formation of the update
:
(update acc product (fn [cities]
(conj cities city)))
Instead of {}
I started out with:
{"Pencil" []
"Bread" []}
That might make it easier to see that for each entry in the table the update
is updating the product key ("Pencil" or "Bread") by putting the latest city on the end (that's what conj
does) of the sequence. When it was working I just replaced with {}
, using the fact that update
will insert a new key if one is not there.
I think of for
as being generative. It takes a sequence as input and at each step generates a new thing - hence you end up with a sequence of new things. There is no updating of product-cities
possible with generation.
reduce
is more useful for what you want to do as you get an 'accumulator' that can be slightly modified at each step. Actually you are creating a new 'accumulator' each time, by modifying the one that is passed in, so in reality you are not modifying anything at all: Clojure being a functional language, its all about creating new things.
Upvotes: 0
Reputation: 1552
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.
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.
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"]}
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)
Upvotes: 5