Lisp (in general) and Clojure (specific case) are different, and require a different way of approaching a problem. Part of learning how to use Clojure to solve problems seems to be unlearning a lot of habits we've acquired when doing imperative programming. In particular, when doing something in an imperative language we often think "How can I start with an empty collection and then add elements to it as I iterate through my data so I end up with the results I want?". This is not good thinking, Clojure-wise. In Clojure the thought process needs to be more along the lines of, "I have one or more collections which contain my data. How can I apply functions to those collections, very likely creating intermediate (and perhaps throw-away) collections along the way, to finally get the collection of results I want?".
OK, let's cut to the chase, and then we'll go back and see why we did what we did. Here's how I modified the original code:
(def santas [{:name "Foo" :email "[email protected]"}
{:name "Bar" :email "[email protected]"}
{:name "Baz" :email "[email protected]"}])
(def kids [{:name "Tommy" :email "[email protected]"}
{:name "Jimmy" :email "[email protected]"}
{:name "Jerry" :email "[email protected]"}
{:name "Johny" :email "[email protected]"}
{:name "Juney" :email "[email protected]"}])
(defn pluck
"Pull out the value of a given key from a seq"
[arr k]
(map #(get % k) arr))
(defn assign-santas [recipients santas]
; Assign kids to santas randomly
; recipients is a shuffled/randomized vector of kids
; santas is a vector of santas
(let [santa-reps (inc (int (/ (count recipients) (count santas)))) ; counts how many repetitions of the santas collection we need to cover the kids
many-santas (flatten (repeat santa-reps santas))] ; repeats the santas collection 'santa-reps' times
(map #(hash-map :santa %1 :kid %2) many-santas recipients)
)
)
(defn assign-santas-main []
(let [recipients (shuffle (pluck kids :email))
pairs (assign-santas recipients (map #(%1 :name) santas))]
; (pprint/pprint pairs)
pairs))
I created a separate collection of kids who are supposed to be assigned randomly to a santa. I also changed it so it creates an assign-santas-main function instead of -main, just for testing purposes.
The only function changed is assign-santas. Instead of starting with an empty collection and then trying to mutate that collection to accumulate the associations we need I did the following:
Determine how many repetitions of the santas collection are needed so we have have at least as many santas as kids (wait - we'll get to it... :-). This is just
TRUNC(#_of_kids / #_of_santas) + 1
or, in Clojure-speak
`(inc (int (/ (count recipients) (count santas))))`
Create a collection which the santas collection repeated as many times as needed (from step 1). This is done with
(flatten (repeat santa-reps santas))
This duplicates (repeat) the santas collection santa-reps times (santa-reps was computed by step 1) and then flatten's it - i.e. takes the elements from all the sub-collections (try executing (repeat 3 santas) and see what you get) and just makes a big flat collection of all the sub-collection's elements.
We then do
(map #(hash-map :santa %1 :kid %2) many-santas recipients)
This says "Take the first element from each of the many-santas and recipients collections, pass them in to the anonymous function given, and then accumulate the results returned by the function into a new collection". (New collection, again - we do that a lot in Clojure). Our little anonymous function says "Create an association (hash-map function), assigning a key of :santa to the first argument I'm given, and a key of :kid to the second argument". The map function then returns that collection of associations.
If you run the assign-santas-main function you get a result which looks like
({:kid "[email protected]", :santa "Foo"}
{:kid "[email protected]", :santa "Bar"}
{:kid "[email protected]", :santa "Baz"}
{:kid "[email protected]", :santa "Foo"}
{:kid "[email protected]", :santa "Bar"})
(I put each association on a separate line - Clojure isn't so gracious when it prints it out - but you get the idea). If you run it again you get something different:
({:kid "[email protected]", :santa "Foo"}
{:kid "[email protected]", :santa "Bar"}
{:kid "[email protected]", :santa "Baz"}
{:kid "[email protected]", :santa "Foo"}
{:kid "[email protected]", :santa "Bar"})
And so on with each different run.
Note that in the rewritten version of assign-santas the entire function could have been written on a single line. I only used a let here to break the calculation of santa-reps and the creation of many-santas out so it was easy to see and explain.
For me, one of the things I find difficult with Clojure (and this is because I'm still very much climbing the learning curve - and for me, with 40+ years of imperative programming experience and habits behind me, this is a pretty steep curve) is just learning the basic functions and how to use them. Some that I find handy on a regular basis are:
map
apply
reduce
I have great difficulty remembering the difference between apply and
reduce. In practice, if one doesn't do what I want I use the other.
repeat
flatten
interleave
partition
hash-map
mapcat
and of course all the "usual" things like +, -, etc.
I'm pretty sure that someone who's more expert than I am at Clojure (not much of a challenge :-) could come up with a way to do this faster/better/cooler, but this might at least give you a different perspective on how to approach this.
Best of luck.
assign-santas, thesantasargument is not used, the firstletbinding is a no-op since it is closed on the same line, andmapdoesn't actually map over a collection. I recommend stripping this code down further, that way you'll probably find the answer by yourself quickest.santasto the map. Not the problem, but certainly a problem :)